Expo UI and Router for iOSExpo SDK 57 and later, iOS only
Expo Router

Tabs

Learn how to use the native tabs layout in Expo Router.

Liquid Glass Tabs with Expo Router — Learn how to use native tabs to create liquid glass tabs on iOS with Expo Router.

Native tabs is in alpha and is available in SDK 54 and later. Its API is subject to change.

Tabs are a common way to navigate between different sections of an app.

  • In Expo Router, you can use different tab layouts, depending on your needs.
  • This guide covers the native tabs.
  • Unlike the other tabs layout, native tabs use the native system tab bar.

For other tab layouts see:

Custom tabs — See custom tabs if your app requires a fully custom design that is not possible using system tabs.

JavaScript tabs — See JavaScript tabs if you already use React Navigation's tabs.

Get started

You can use file-based routing to create a tabs layout. Here's an example file structure:

  • src
    • app
      • _layout.tsx
      • index.tsx
      • settings.tsx

The above file structure produces a layout with a tab bar at the bottom of the screen. The tab bar will have two tabs: Home and Settings.

You can use the src/app/_layout.tsx file to define your app's root layout using tabs. This file is the main layout file for the tab bar and each tab. Inside it, you can control how the tab bar and each tab item look and behave.

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon sf="house.fill" md="home" />
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="settings">
        <NativeTabs.Trigger.Icon sf="gear" md="settings" />
        <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Customizing tab bar items

When you want to customize the tab bar item, we recommend using the components API designed for this purpose. Currently, you can customize:

  • Icon: The icon displayed in the tab bar item.
  • Label: The label displayed in the tab bar item.
  • Badge: The badge displayed in the tab bar item.

Icon

NativeTabs.Trigger.Icon is available in SDK 55 and later. For SDK 54, use Icon imported from expo-router/unstable-native-tabs.

You can use the Icon component to customize the icon displayed in the tab bar item. The Icon component accepts a md prop for Android material symbols, a sf prop for Apple's SF Symbols icons, or a src prop for custom images.

Alternatively, you can pass {default: ..., selected: ...} to the sf, xcasset, drawable, md, or src prop to specify different icons for the default and selected states.

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Icon
          sf={{ default: 'house', selected: 'house.fill' }}
          md={{ default: 'home', selected: 'home_filled' }}
        />
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="settings">
        <NativeTabs.Trigger.Icon src={require('../../../assets/setting_icon.png')} />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}
import { DynamicColorIOS } from 'react-native';
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs
      labelStyle={{
        // For the text color
        color: DynamicColorIOS({
          dark: 'white',
          light: 'black',
        }),
      }}
      // For the selected icon color
      tintColor={DynamicColorIOS({
        dark: 'white',
        light: 'black',
      })}>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Icon sf={{ default: 'house', selected: 'house.fill' }} md="home" />
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="settings">
        <NativeTabs.Trigger.Icon
          src={{
            default: require('../assets/setting_icon.png'),
            selected: require('../assets/selected_setting_icon.png'),
          }}
        />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Icon rendering mode

Icon rendering mode is available in SDK 55 and later.

When using the src or xcasset prop for custom images on iOS, you can control how the icon is rendered with the renderingMode prop:

  • template (default): The icon is rendered as a template image, allowing iOS to apply the tint color. This is ideal for single-color icons that should match your app's color scheme.
  • original: The icon is rendered with its original colors preserved. This is useful for icons with gradients or multiple colors.
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      {/* Icon with original colors preserved (e.g., for gradient or multi-color icons) */}
      <NativeTabs.Trigger name="colorful">
        <NativeTabs.Trigger.Icon
          src={require('../../../assets/colorful_icon.png')}
          renderingMode="original"
        />
      </NativeTabs.Trigger>
      {/* Icon rendered as a template (default behavior) */}
      <NativeTabs.Trigger name="simple">
        <NativeTabs.Trigger.Icon
          src={require('../../../assets/simple_icon.png')}
          renderingMode="template"
        />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Asset catalog icons (iOS)

This feature is available in SDK 55 and later.

On iOS, you can use images from the Xcode asset catalog as tab icons with the xcasset prop. This is useful when you want to manage your icons through Xcode's asset catalog instead of bundling image files.

Pass a string with the asset name to use the same icon for both default and selected states:

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Icon xcasset="home-icon" />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

To use different icons for default and selected states, pass an object:

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Icon
          xcasset={{
            default: 'home-outline',
            selected: 'home-filled',
          }}
        />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Asset catalog icons support the renderingMode prop, just like src icons. When iconColor is set, icons default to template rendering. Otherwise, they default to original.

Vector icons

You can render icons from an icon font, such as the ones provided by react-native-vector-icons, by passing an image source to the src prop. Each icon set exposes a getImageSourceSync method that rasterizes a glyph to an image source you can pass directly to src.

This is useful on Android, where the built-in md prop only renders outlined Material Symbols. An icon font like Material Design Icons provides both outlined and filled glyphs (for example, home-outline and home), so you can show distinct default and selected icons.

To start, install the icon set you want to use along with @react-native-vector-icons/get-image, which provides the native module that getImageSourceSync relies on. The example below uses the @react-native-vector-icons/material-design-icons icon set:

bun expo install @react-native-vector-icons/material-design-icons @react-native-vector-icons/get-image

getImageSourceSync requires a development build. Rebuild your app after installing the packages so the native module and icon font are bundled.

getImageSourceSync is synchronous, so compute the image sources once at module scope rather than on each render.

  • Combine src with sf to use the vector icon on Android and SF Symbols on iOS.
  • On iOS, sf takes precedence over src; on Android, the icon falls back to src.
import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons';
import { NativeTabs } from 'expo-router/unstable-native-tabs';

const homeIcon = MaterialDesignIcons.getImageSourceSync('home', 24, 'black');
const starOutlineIcon = MaterialDesignIcons.getImageSourceSync('star-outline', 24, 'black');
const starIcon = MaterialDesignIcons.getImageSourceSync('star', 24, 'black');

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        {/* `sf` is used on iOS, `src` (the vector icon) on Android. */}
        <NativeTabs.Trigger.Icon sf="house" src={homeIcon} />
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="explore">
        {/* Outlined when unselected, filled when selected. */}
        <NativeTabs.Trigger.Icon
          sf={{ default: 'star', selected: 'star.fill' }}
          src={{ default: starOutlineIcon, selected: starIcon }}
        />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Label

You can use the Label component to customize the label displayed in the tab bar item. The Label component accepts a string label passed as a child. If no label is provided, the tab bar item will use the route name as the label.

If you don't want to display a label, you can use the hidden prop to hide the label.

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="settings">
        <NativeTabs.Trigger.Label hidden />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Badge

You can use the Badge component to customize the badge displayed for the tab bar item. The badge is an additional mark on top of the tab and useful for showing notification or unread message counts.

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="messages">
        <NativeTabs.Trigger.Badge>9+</NativeTabs.Trigger.Badge>
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="settings">
        <NativeTabs.Trigger.Badge />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Customizing the tab bar

Since the native tab layout's appearance varies by platform, the customization options are also different. For all customization options, see the API reference for NativeTabs.

Advanced

Hiding the Tab bar

hidden property is available in SDK 55 and later.

You can hide the tab bar using hidden prop on the NativeTabs component. To hide tab bar for specific screens, you can use context API to set the hidden prop dynamically.

import { createContext } from 'react';

export const TabBarContext = createContext<{
  setIsTabBarHidden: (hidden: boolean) => void;
}>({
  setIsTabBarHidden: () => {},
});
import { NativeTabs } from 'expo-router/unstable-native-tabs';
import { useState } from 'react';

import { TabBarContext } from '@/context/TabBarContext';

export default function TabLayout() {
  const [isTabBarHidden, setIsTabBarHidden] = useState(false);
  return (
    <TabBarContext value={{ setIsTabBarHidden }}>
      <NativeTabs hidden={isTabBarHidden}>
        <NativeTabs.Trigger name="index">
          <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
        </NativeTabs.Trigger>
        <NativeTabs.Trigger name="settings">
          <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
        </NativeTabs.Trigger>
      </NativeTabs>
    </TabBarContext>
  );
}
import { useFocusEffect } from 'expo-router';
import { use } from 'react';

import { TabBarContext } from '@/context/TabBarContext';

export default function HomeScreen() {
  const { setIsTabBarHidden } = use(TabBarContext);

  useFocusEffect(() => {
    setIsTabBarHidden(true);
    return () => setIsTabBarHidden(false);
  });

  return (
    // Screen content
  );
}

Hiding a tab conditionally

Dynamically hiding tabs will remount the navigator and the state will be reset. Change the visibility of the tabs only before the navigator is mounted or when it is not visible to the user.

If you want to hide a tab based on a condition, you can either remove the trigger or pass the hidden prop to the NativeTabs.Trigger component.

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  const shouldHideMessagesTab = true; // Replace with your condition
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="messages" hidden={shouldHideMessagesTab} />
    </NativeTabs>
  );
}

Note: Marking a tab as hidden means it cannot be navigated to in any way.

Dismiss behavior

By default, tapping a tab that is already active closes all screens in that tab's stack and returns to the root screen. You can disable this by setting the disablePopToTop prop on the NativeTabs.Trigger component.

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index" disablePopToTop>
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="settings">
        <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Scroll to top

By default, tapping a tab that is already active and showing its root screen scrolls the content back to the top. You can disable this by setting the disableScrollToTop prop on the NativeTabs.Trigger component.

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index" disableScrollToTop>
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="settings">
        <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Disabled tabs

The disabled prop is available in SDK 56 and later.

You can prevent native selection of a tab by setting the disabled prop on the NativeTabs.Trigger component.

  • When true, tapping the tab in the tab bar does not change the focused tab.
  • The tab remains visible - use hidden if you want to remove it from the tab bar entirely.

Note: disabled only suppresses the native tap interaction. It is not a "protected" or authorization gate - JavaScript navigation such as router.push('/settings') or <Link href="/settings" /> still navigates to the tab.

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="settings" disabled>
        <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

You can also toggle disabled dynamically from inside a screen.

import { NativeTabs } from 'expo-router/unstable-native-tabs';
import { View } from 'react-native';

export default function CheckoutScreen() {
  const isProcessing = useIsProcessing();
  return (
    <View>
      <NativeTabs.Trigger disabled={isProcessing} />
      {/* ... */}
    </View>
  );
}

iOS 26 features

To use features described in this section, compile your app with Xcode 26 or higher.

Separate search tab

To add a separate search tab, assign the role with its value set to search to the native tab you want to display separately.

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="search" role="search">
        <NativeTabs.Trigger.Label>Search</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Tabbar search input

To add a search field to the tab bar, wrap the screen in a Stack navigator and configure headerSearchBarOptions.

  • src
    • app
      • _layout.tsx
      • index.tsx
      • search
        • _layout.tsx
        • index.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="search" role="search">
        <NativeTabs.Trigger.Label>Search</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}
import { Stack } from 'expo-router';

export default function SearchLayout() {
  return <Stack />;
}
import { ScrollView } from 'react-native';
import { Stack } from 'expo-router';

export default function SearchIndex() {
  return (
    <>
      <Stack.Title>Search</Stack.Title>
      <Stack.SearchBar placement="automatic" placeholder="Search" onChangeText={() => {}} />
      <ScrollView>{/* Screen content */}</ScrollView>
    </>
  );
}

Tab bar minimize behavior

To implement the minimized behavior on the tab bar, you can use minimizeBehavior prop on NativeTabs. In the example below, the tab bar is minimized when scrolling down.

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs minimizeBehavior="onScrollDown">
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="tab-1">
        <NativeTabs.Trigger.Label>Tab 1</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Bottom accessory

This feature is available in SDK 55 and later.

A bottom accessory is a floating view that appears above the tab bar, useful for displaying persistent controls like a mini music player. See Apple's UITabBarController bottomAccessory documentation for more details.

The bottom accessory can appear in two placements: 'regular' (standard position above the tab bar) or 'inline' (compact mode, inline with the tab bar). Use the usePlacement hook to adapt your UI based on the current placement.

You must store state outside the accessory component using props, context, or external state management. Two instances of the bottom accessory component are rendered simultaneously (one for each placement) and state is not shared between them.

The following example demonstrates a mini player with state lifted to the parent component:

import { NativeTabs } from 'expo-router/unstable-native-tabs';
import { useState } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';

function MiniPlayer({ isPlaying, onToggle }) {
  const placement = NativeTabs.BottomAccessory.usePlacement();

  if (placement === 'inline') {
    // Compact UI for inline placement
    return (
      <Pressable onPress={onToggle} style={styles.inlinePlayer}>
        <Text>{isPlaying ? '⏸' : '▶'}</Text>
      </Pressable>
    );
  }

  // Full UI for regular placement
  return (
    <View style={styles.regularPlayer}>
      <Text>Now Playing: Song Title</Text>
      <Pressable onPress={onToggle}>
        <Text>{isPlaying ? 'Pause' : 'Play'}</Text>
      </Pressable>
    </View>
  );
}

export default function TabLayout() {
  // State must be stored outside BottomAccessory
  const [isPlaying, setIsPlaying] = useState(false);

  return (
    <NativeTabs>
      <NativeTabs.BottomAccessory>
        <MiniPlayer isPlaying={isPlaying} onToggle={() => setIsPlaying(!isPlaying)} />
      </NativeTabs.BottomAccessory>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="library">
        <NativeTabs.Trigger.Label>Library</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

const styles = StyleSheet.create({
  inlinePlayer: {
    padding: 8,
  },
  regularPlayer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: 16,
  },
});

Safe area handling

This feature is available in SDK 55 and later.

Native tabs automatically handle safe area insets, with platform-specific behavior:

Disabling automatic content insets

If you need full control over safe area handling, you can disable automatic content inset adjustment using the disableAutomaticContentInsets prop on NativeTabs.Trigger:

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index" disableAutomaticContentInsets>
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Lazy loading

All tab screens in native tabs render eagerly when the navigator mounts.

  • This behavior cannot be changed because the native tab bar needs each screen to be available for transitions.
  • If a tab contains expensive content that you want to defer until the user actually visits the tab, you can use one of the following approaches.

Render content only when focused

Use useIsFocused to conditionally render content. The content unmounts when the user navigates away and re-renders when they come back. This means any local state (scroll position, form inputs) is lost on every tab switch.

import { useIsFocused } from 'expo-router';
import { View, ActivityIndicator, Text } from 'react-native';

export default function SearchScreen() {
  const isFocused = useIsFocused();

  if (!isFocused) {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <ActivityIndicator />
      </View>
    );
  }

  return (
    <View style={{ flex: 1 }}>
      <Text>Expensive content that only renders when this tab is focused</Text>
    </View>
  );
}

Load once on first focus

Use useFocusEffect with a state flag to load content the first time the tab is focused, then keep it mounted.

import { useFocusEffect } from 'expo-router';
import { useCallback, useState } from 'react';
import { View, ActivityIndicator, Text } from 'react-native';

export default function SearchScreen() {
  const [hasActivated, setHasActivated] = useState(false);

  useFocusEffect(
    useCallback(() => {
      setHasActivated(true);
    }, [])
  );

  if (!hasActivated) {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <ActivityIndicator />
      </View>
    );
  }

  return (
    <View style={{ flex: 1 }}>
      <Text>Content that loads once and stays mounted</Text>
    </View>
  );
}

Migrating from JavaScript tabs

Native tabs are not designed to be a drop-in replacement for JavaScript tabs.

  • The native tabs are constrained to the native platform behavior, whereas the JavaScript tabs can be customized more freely.
  • If you aren't interested in the native platform behavior, you can continue using the JavaScript tabs.

Use Trigger instead of Screen

NativeTabs introduces the concept of a Trigger for adding routes to a layout. Unlike a Screen, which styles routes that are added automatically, the Trigger system gives you better control for hiding and removing tabs from the tab bar.

Use React components instead of props

NativeTabs has a React-first API that opts to use components for defining UI in favor of props objects.

Use Stacks inside tabs

The JavaScript <Tabs /> have a mock stack header which is not present in the native tabs. Instead, you should nest a native <Stack /> layout inside the native tabs to support both headers and pushing screens.

Common problems

The tab bar is transparent on iOS 18 and earlier

On iOS 18 and earlier, the native tab bar becomes transparent when scrolling to the end of a scrollable content. This means that it will become transparent when you scroll to the end of a ScrollView or when you render a static View.

You can use the disableTransparentOnScrollEdge prop to disable this behavior.

import { NativeTabs } from 'expo-router/unstable-native-tabs';

function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index" disableTransparentOnScrollEdge>
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

When you are using a ScrollView and the tab bar is transparent from the start, ensure that the ScrollView is a first child of the screen component. If you wrap it with another component make sure to set collapsable to false on the wrapper component.

import { ScrollView, View } from 'react-native';

export default function HomeScreen() {
  return (
    <View collapsable={false} style={{ flex: 1 }}>
      <ScrollView>{/* Screen content */}</ScrollView>
    </View>
  );
}

White background flashes when switching tabs on iOS 26

This happens because the default theme uses a white background color. To fix this, wrap your app in Expo Router's ThemeProvider with the appropriate theme.

ThemeProvider, DarkTheme, and DefaultTheme are exported from expo-router in SDK 56 and later. For SDK 55, import them from @react-navigation/native instead.

For apps supporting both light and dark modes:

import { ThemeProvider, DarkTheme, DefaultTheme } from 'expo-router';
import { NativeTabs } from 'expo-router/unstable-native-tabs';
import { useColorScheme } from 'react-native';

export default function TabLayout() {
  const colorScheme = useColorScheme();

  return (
    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
      <NativeTabs>
        <NativeTabs.Trigger name="index">
          <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
        </NativeTabs.Trigger>
        <NativeTabs.Trigger name="settings">
          <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
        </NativeTabs.Trigger>
      </NativeTabs>
    </ThemeProvider>
  );
}

For dark-mode-only apps:

import { ThemeProvider, DarkTheme } from 'expo-router';
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <ThemeProvider value={DarkTheme}>
      <NativeTabs>{/* tabs */}</NativeTabs>
    </ThemeProvider>
  );
}

Alternative for specific background colors:

If you need a specific background color that doesn't match the default themes, you can use the contentStyle prop on NativeTabs.Trigger:

<NativeTabs.Trigger name="index" contentStyle={{ backgroundColor: '#1a1a2e' }}>

The tab bar background props have no effect on iOS 26

On iOS 26 and later, the system draws the tab bar with Liquid Glass and derives its background from the content behind it. The backgroundColor, blurEffect, shadowColor, and disableTransparentOnScrollEdge props affect the iOS tab bar only on iOS 18 and earlier.

This also means the tab bar does not follow a color scheme that exists only in JavaScript. Setting the color scheme with Appearance.setColorScheme changes the interface style that other native views resolve against, but it does not change the tab bar background either.

To make the tab bar match a dark UI on iOS 26, make the content behind the tab bar dark, as described in White background flashes when switching tabs on iOS 26.

Scroll to top does not work when tapping a tab

Tapping an active tab should scroll the content to the top, but this may not work if the ScrollView is not the first child of the screen component.

Ensure that the ScrollView is a direct first child of the screen component. If you wrap it with another component, make sure to set collapsable to false on the wrapper component.

import { ScrollView, View } from 'react-native';

export default function HomeScreen() {
  return (
    <View collapsable={false} style={{ flex: 1 }}>
      <ScrollView>{/* Screen content */}</ScrollView>
    </View>
  );
}

Liquid glass header buttons flicker in dark mode on iOS 26

Header buttons with liquid glass styling may flicker or flash their background when switching tabs in dark mode on iOS 26. This happens because the default theme doesn't match the system dark mode, causing visual artifacts in the liquid glass rendering.

The fix is the same as for the white background flash issue: wrap your layout with <ThemeProvider> from expo-router using the appropriate theme.

ThemeProvider, DarkTheme, and DefaultTheme are exported from expo-router in SDK 56 and later. For SDK 55, import them from @react-navigation/native instead.

For apps supporting both light and dark modes:

import { ThemeProvider, DarkTheme, DefaultTheme } from 'expo-router';
import { NativeTabs } from 'expo-router/unstable-native-tabs';
import { useColorScheme } from 'react-native';

export default function TabLayout() {
  const colorScheme = useColorScheme();

  return (
    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
      <NativeTabs>
        <NativeTabs.Trigger name="index">
          <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
        </NativeTabs.Trigger>
        <NativeTabs.Trigger name="settings">
          <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
        </NativeTabs.Trigger>
      </NativeTabs>
    </ThemeProvider>
  );
}

For dark-mode-only apps:

import { ThemeProvider, DarkTheme } from 'expo-router';
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <ThemeProvider value={DarkTheme}>
      <NativeTabs>{/* tabs */}</NativeTabs>
    </ThemeProvider>
  );
}

Known limitations

The default and selected icons share one rendering mode on iOS

On iOS, a tab's default and selected image icons must use the same rendering mode. When they resolve to different modes, both icons use the default icon's mode and Expo Router logs a warning in development.

The modes disagree when an icon color applies to only one of the states.

  • This happens when you set tintColor, iconColor={{ selected }}, or the Icon selectedColor prop without also setting a color for the default state.
  • A color implies 'template' rendering, while an uncolored icon defaults to 'original'.
  • Setting renderingMode for only one state has the same effect.

To render both icons the same way, set a color for both states, or set renderingMode on the Icon:

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    // `iconColor` applies to both states, so both icons render as templates
    <NativeTabs iconColor={{ default: 'gray', selected: 'black' }}>
      <NativeTabs.Trigger name="settings">
        <NativeTabs.Trigger.Icon
          src={{
            default: require('../assets/setting_icon.png'),
            selected: require('../assets/selected_setting_icon.png'),
          }}
        />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

This limitation doesn't apply to SF Symbols, which the system always tints.

Cannot measure the tab bar height

The tabs move around, sometimes being on top of the screen when rendering on iPad, sometimes on the side of the screen when running on Apple Vision Pro, and so on. We're working on a layout function to provide more detailed layout info in the future.

No support for nested native tabs

Native tabs cannot be nested inside other native tabs. You can still nest JavaScript tabs inside native tabs.

Limited support for FlatList

FlatList integration with native tabs has limitations.

  • Features like scroll-to-top and minimize-on-scroll aren't supported.
  • Additionally, detecting scroll edges may fail, causing the tab bar to appear transparent.
  • To fix this, use the 0 prop.
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs disableTransparentOnScrollEdge>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

No support for dynamically adding or removing tabs

Dynamically adding or removing tabs at runtime is not supported.

  • Tabs should be defined statically in your layout file and remain consistent throughout the app's lifecycle.
  • This aligns with platform guidelines from Apple's Human Interface Guidelines which recommend keeping tab bar content stable to help users build a mental model of your app's navigation structure.
  • If you dynamically add or remove tabs, the content will be remounted and the state will be lost.

Native tabs API

expo-router/unstable-native-tabs is a submodule of expo-router and exports components to build tab layouts using platform-native system tabs.

Installation

To use expo-router/unstable-native-tabs in your project, you need to install expo-router in your project. Follow the instructions from Expo Router's installation guide:

Install Expo Router — Learn how to install Expo Router in your project.

Configuration in app config

If you are using the default template to create a new project, expo-router's config plugin is already configured in your app config.

Example app.json with config plugin

{
  "expo": {
    "plugins": ["expo-router"]
  }
}

Usage

To learn how to use native tabs, with Expo Router, read the native tabs guide:

Native tabs — Learn how to use native tabs in your Expo Router app.

API

import { NativeTabs } from 'expo-router/unstable-native-tabs';

Components

NativeTabs

Type: React.Element<NativeTabsProps>

The component used to create native tabs layout.

Example

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function Layout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="home" />
      <NativeTabs.Trigger name="settings" />
    </NativeTabs>
  );
}

NativeTabsProps

backgroundColor

Optional • Type: ColorValue

The background color of the tab bar.

badgeBackgroundColor

Optional • Type: ColorValue

The background color of every badge in the tab bar.

blurEffect

Optional • Literal type: string

The blur effect applied to the tab bar.

Acceptable values are: 'none' | 'regular' | 'light' | 'dark' | 'systemDefault' | 'extraLight' | 'prominent' | 'systemUltraThinMaterial' | 'systemThinMaterial' | 'systemMaterial' | 'systemThickMaterial' | 'systemChromeMaterial' | 'systemUltraThinMaterialLight' | 'systemThinMaterialLight' | 'systemMaterialLight' | 'systemThickMaterialLight' | 'systemChromeMaterialLight' | 'systemUltraThinMaterialDark' | 'systemThinMaterialDark' | 'systemMaterialDark' | 'systemThickMaterialDark' | 'systemChromeMaterialDark'

disableTransparentOnScrollEdge

Optional • Type: boolean

When set to true, the tab bar will not become transparent when scrolled to the edge.

hidden

Optional • Type: boolean • Default: false

When set to true, hides the tab bar.

iconColor

Optional • Literal type: union

The color of every tab icon in the tab bar.

Acceptable values are: ColorValue | { default: ColorValue | undefined, selected: ColorValue | undefined }

labelStyle

Optional • Literal type: union

The style of the every tab label in the tab bar.

Acceptable values are: StyleProp<NativeTabsLabelStyle> | { default: StyleProp<NativeTabsLabelStyle>, selected: StyleProp<NativeTabsLabelStyle> }

minimizeBehavior

iOS 26+

Optional • Literal type: string • Default: automatic

Specifies the minimize behavior for the tab bar.

Available starting from iOS 26.

The following values are currently supported:

  • automatic - resolves to the system default minimize behavior
  • never - the tab bar does not minimize
  • onScrollDown - the tab bar minimizes when scrolling down and expands when scrolling back up
  • onScrollUp - the tab bar minimizes when scrolling up and expands when scrolling back down

See: The supported values correspond to the official Apple documentation.

Acceptable values are: 'automatic' | 'never' | 'onScrollDown' | 'onScrollUp'

screenListeners

Optional • Literal type: union

Listeners for navigation events on all tabs.

Supported events:

  • tabPress - called when a tab is pressed
  • focus - called when the screen comes into focus
  • blur - called when the screen loses focus

Example

<NativeTabs
  screenListeners={{
    tabPress: (e) => {
      console.log('Any tab pressed');
    },
  }}
>
  ...
</NativeTabs>

Acceptable values are: (prop: { route: RouteProp<ParamListBase, string> }) => ScreenListeners<TabNavigationState<ParamListBase>, NativeTabNavigationEventMap> | Partial<{ beforeRemove: EventListenerCallback<NativeTabNavigationEventMap & EventMapCore<TabNavigationState<ParamListBase>>, 'beforeRemove', true>, blur: EventListenerCallback<NativeTabNavigationEventMap & EventMapCore<TabNavigationState<ParamListBase>>, 'blur', unknown>, focus: EventListenerCallback<NativeTabNavigationEventMap & EventMapCore<TabNavigationState<ParamListBase>>, 'focus', unknown>, state: EventListenerCallback<NativeTabNavigationEventMap & EventMapCore<TabNavigationState<ParamListBase>>, 'state', unknown>, tabPress: EventListenerCallback<NativeTabNavigationEventMap & EventMapCore<TabNavigationState<ParamListBase>>, 'tabPress', false> }>

shadowColor

Optional • Type: ColorValue

The color of the shadow.

See: Apple documentation

sidebarAdaptable

iOS 18+

Optional • Type: boolean

When set to true, enables the sidebarAdaptable tab bar style on iPadOS and macOS. This prop has no effect on iPhone.

tintColor

Optional • Type: ColorValue

The tint color of the tab icon.

Can be overridden by icon color and label color for each tab individually.

titlePositionAdjustment

Optional • Type: { horizontal: number, vertical: number }

See: Apple documentation

unstable_nativeProps

Optional • Type: Partial<Omit<TabsHostProps, 'children' | 'navStateRequest' | 'onTabSelected'>>

Props passed to the underlying native tab host implementation in react-native-screens. Use this to configure props that are not directly exposed by Expo Router.

Note: This is an unstable API and may change or be removed in minor versions.

Inherited props
  • PropsWithChildren

NativeTabs.Trigger

Type: React.Element<NativeTabTriggerProps>

The component used to customize the native tab options both in the _layout file and from the tab screen.

When used in the _layout file, you need to provide a name prop. When used in the tab screen, the name prop takes no effect.

Example

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function Layout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="home" />
      <NativeTabs.Trigger name="settings" />
    </NativeTabs>
  );
}

Example

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function HomeScreen() {
  return (
    <View>
      <NativeTabs.Trigger>
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
      <Text>This is home screen!</Text>
    </View>
  );
}

NativeTabTriggerProps

children

Optional • Type: ReactNode

The children of the trigger.

Use Icon, Label, and Badge components to customize the tab.

contentStyle

Optional • Type: Pick<ViewStyle, 'backgroundColor' | 'experimental_backgroundImage' | 'alignContent' | 'alignItems' | 'flexDirection' | 'gap' | 'justifyContent' | 'padding' | 'paddingBottom' | 'paddingEnd' | 'paddingHorizontal' | 'paddingLeft' | 'paddingRight' | 'paddingStart' | 'paddingTop' | 'paddingVertical' | 'paddingBlock' | 'paddingBlockEnd' | 'paddingBlockStart' | 'paddingInline' | 'paddingInlineEnd' | 'paddingInlineStart'>

The style applied to the content of the tab

Note: Only certain style properties are supported.

disableAutomaticContentInsets

Optional • Type: boolean

The default behavior differs between iOS and Android.

On iOS, the first scroll view nested inside a native tabs screen has automatic content inset adjustment enabled

When this property is set to true, automatic content inset adjustment is disabled for the screen and must be managed manually. You can use SafeAreaView from react-native-screens/experimental to handle safe area insets.

disabled

Optional • Type: boolean • Default: false

If true, the tab is shown but cannot be selected by tapping it in the tab bar.

Note: This only suppresses the native tap interaction. JavaScript navigation such as router.push() or <Link /> still navigates to the tab. Use this for tabs that should appear visible but be temporarily inert, and gate navigation in your own code if you need to fully prevent access.

Unlike hidden, the tab remains visible in the tab bar.

disablePopToTop

Optional • Type: boolean • Default: false

If true, the tab will not pop stack to the root when selected again.

disableScrollToTop

Optional • Type: boolean • Default: false

If true, the tab will not scroll to the top when selected again.

disableTransparentOnScrollEdge

Optional • Type: boolean

When set to true, the tab bar will not become transparent when scrolled to the edge.

When set on a trigger, it takes precedence over the value set on NativeTabs.

hidden

Optional • Type: boolean

If true, the tab will be hidden from the tab bar.

Note: Marking a tab as hidden means it cannot be navigated to in any way.

Note: Dynamically hiding tabs will remount the navigator and the state will be reset.

listeners

Optional • Literal type: union

Listeners for navigation events on this tab.

Supported events:

  • tabPress - called when this tab is pressed
  • focus - called when this screen comes into focus
  • blur - called when this screen loses focus

Example

<NativeTabs.Trigger
  name="home"
  listeners={{
    tabPress: (e) => {
      console.log('Home tab pressed');
    },
  }}
/>

Acceptable values are: Partial<{ beforeRemove: EventListenerCallback<NativeTabNavigationEventMap & EventMapCore<TabNavigationState<ParamListBase>>, 'beforeRemove', true>, blur: EventListenerCallback<NativeTabNavigationEventMap & EventMapCore<TabNavigationState<ParamListBase>>, 'blur', unknown>, focus: EventListenerCallback<NativeTabNavigationEventMap & EventMapCore<TabNavigationState<ParamListBase>>, 'focus', unknown>, state: EventListenerCallback<NativeTabNavigationEventMap & EventMapCore<TabNavigationState<ParamListBase>>, 'state', unknown>, tabPress: EventListenerCallback<NativeTabNavigationEventMap & EventMapCore<TabNavigationState<ParamListBase>>, 'tabPress', false> }> | (prop: { navigation: any, route: RouteProp<ParamListBase, string> }) => ScreenListeners<TState, TEventMap>

name

Optional • Type: string

The name of the route.

This is required when used inside a Layout component.

When used in a route it has no effect.

role

Optional • Literal type: string

System-provided tab bar item with predefined icon and title

Uses Apple's built-in tab bar items (e.g., bookmarks, contacts, downloads) with standard iOS styling and localized titles. Custom icon or selectedIcon properties will override the system icon, but the system-defined title cannot be customized.

See: The supported values correspond to the official Apple documentation.

Acceptable values are: 'search' | 'history' | 'bookmarks' | 'contacts' | 'downloads' | 'favorites' | 'featured' | 'more' | 'mostRecent' | 'mostViewed' | 'recents' | 'topRated'

unstable_nativeProps

Optional • Type: Partial<Omit<TabsScreenProps, 'screenKey'>>

Props passed to the underlying native tab screen implementation. Use this to configure props not directly exposed by Expo Router, but available in react-native-screens.

Note: This will override any other props set by Expo Router and may lead to unexpected behavior.

Note: This is an unstable API and may change or be removed in minor versions.

NativeTabs.BottomAccessory

Type: React.Element<FC<NativeTabsBottomAccessoryProps> & { usePlacement: () => 'regular' | 'inline' }>

NativeTabsBottomAccessoryProps

children

Optional • Type: ReactNode

NativeTabs.Trigger.Badge

Type: React.Element<FC<NativeTabsTriggerBadgeProps>>

NativeTabsTriggerBadgeProps

children

Optional • Type: string

The text to display as the badge for the tab. If not provided, the badge will not be displayed.

hidden

Optional • Type: boolean • Default: false

If true, the badge will be hidden.

selectedBackgroundColor

Optional • Type: ColorValue

NativeTabs.Trigger.Icon

Type: React.Element<FC<NativeTabsTriggerIconProps>>

NativeTabsTriggerIconProps

selectedColor

Optional • Type: ColorValue

NativeTabs.Trigger.Label

Type: React.Element<FC<NativeTabsTriggerLabelProps>>

NativeTabsTriggerLabelProps

children

Optional • Type: string

The text to display as the label for the tab.

hidden

Optional • Type: boolean • Default: false

If true, the label will be hidden.

selectedStyle

Optional • Type: StyleProp<NativeTabsLabelStyle>

NativeTabs.Trigger.VectorIcon

Type: React.Element<VectorIconProps<NameT>>

Helper component for loading vector icons.

Prefer using the md and sf props on Icon rather than using this component directly. Only use this component when you need to load a specific icon from a vector icon family.

Example

import { Icon, VectorIcon } from 'expo-router';
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';

<Icon src={<VectorIcon family={MaterialCommunityIcons} name="home" />} />

Interfaces

DrawableIcon

PropertyTypeDescription
  • As a string with the drawable resource name
  • As an object specifying the default and selected states

. Example.

<Icon drawable="ic_home" />

. Example.

<Icon drawable={{ default: 'ic_home_outline', selected: 'ic_home_filled' }} />

|

SFSymbolIcon

PropertyTypeDescription
sf(optional)SFSymbols7_0 | { default: SFSymbols7_0 | undefined, selected: SFSymbols7_0 }The name of the SF Symbol to use as an icon. The value can be provided in two ways
  • As a string with the SF Symbol name
  • As an object specifying the default and selected states

. Example.

<Icon sf="magnifyingglass" />

. Example.

<Icon sf={{ default: "house", selected: "house.fill" }} />

|

SrcIcon

PropertyTypeDescription
renderingMode(optional)'template' | 'original'Controls how the image icon is rendered on iOS
  • 'template': iOS applies tint color to the icon (selected/unselected states)
  • 'original': Preserves original icon colors

. Default behavior:

  • If tab bar icon color is configured, defaults to 'template'
  • If no icon color is set, defaults to 'original'

.

  • See: Apple documentation for more information. | | src(optional) | ReactElement<unknown, string | JSXElementConstructor<any>> | ImageSourcePropType | { default: ReactElement<unknown, string | JSXElementConstructor<any>> | ImageSourcePropType | undefined, selected: ReactElement<unknown, string | JSXElementConstructor<any>> | ImageSourcePropType } | The image source to use as an icon.

  • When sf prop is used it will override this prop on iOS.

  • When drawable or material prop is used it will override this prop on Android.

  • The value can be provided in two ways|

  • As an image source

  • As an object specifying the default and selected states

. Example.

<Icon src={require('./path/to/icon.png')} />

. Example.

<Icon src={{ default: require('./path/to/icon.png'), selected: require('./path/to/icon-selected.png') }} />

|

XcassetIcon

PropertyTypeDescription
xcasset(optional)string | { default: string, selected: string }The name of the iOS asset catalog image to use as an icon. Xcassets provide automatic multi-resolution (@1x/@2x/@3x), dark mode variants, and device-specific images via [UIImage imageNamed:]. The rendering mode (template vs original) can be controlled via the renderingMode prop on the Icon component. By default, icons are tinted when iconColor is set, and rendered as original otherwise. The value can be provided in two ways
  • As a string with the asset catalog image name
  • As an object specifying the default and selected states

. Example.

<Icon xcasset="custom-icon" />

. Example.

<Icon xcasset={{ default: "home-outline", selected: "home-filled" }} />

|

Types

NativeTabsLabelStyle

Type: Pick<TextStyle, 'fontFamily' | 'fontSize' | 'fontStyle' | 'fontWeight' | 'color'>

SymbolOrImageSource

Type: object shaped as below:

PropertyTypeDescription
sf(optional)SFSymbolThe name of the SF Symbol to use as an icon.
xcasset(optional)stringThe name of the iOS asset catalog image to use as an icon.

Or object shaped as below:

PropertyTypeDescription
renderingMode(optional)'template' | 'original'Controls how the icon is rendered on iOS. Default: 'template'
src(optional)ImageSourcePropType | Promise<ImageSourcePropType | null>The image source to use as an icon.

JavaScript tabs

Using a JavaScript Tab Navigator with Expo Router — Configure the tab icons, nest navigators, and manage navigation history.

Tabs are a common way to navigate between different sections of an app.

  • Expo Router provides a tabs layout to help you create a tab bar at the bottom of your app.
  • The fastest way to get started is to use a template.
  • See the quick start installation to get started.

Multiple tab layouts

Expo Router offers three types of tab navigators:

  • JavaScript tabs: It is implemented with React Navigation's bottom tabs and offers familiar API if you have already used React Navigation.
  • Native tabs: It uses a platform's native tab bar and offers native look and feel.
  • Custom tabs: It provides headless tab components from expo-router/ui to build a fully custom tab layout to achieve complex UI patterns.

This guide covers the JavaScript tabs layout. For other tab layouts see:

Native tabs — See native tabs if you want to achieve a native look and feel for your tab bar.

Custom tabs — See custom tabs if your app requires a fully custom design that is not possible using system tabs.

Get started with JavaScript tabs

You can use file-based routing to create a tabs layout. Here's an example file structure:

  • src
    • app
      • _layout.tsx
      • (tabs)
        • _layout.tsx
        • index.tsx
        • settings.tsx

This file structure produces a layout with a tab bar at the bottom of the screen. The tab bar will have two tabs: Home and Settings:

You can use the src/app/_layout.tsx file to define your app's root layout:

import { Stack } from 'expo-router';

export default function Layout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
    </Stack>
  );
}

The (tabs) directory is a special directory name that tells Expo Router to use the Tabs layout.

From the file structure, the (tabs) directory has three files. The first is (tabs)/_layout.tsx. This file is the main layout file for the tab bar and each tab. Inside it, you can control how the tab bar and each tab button look and behave.

import FontAwesome from '@expo/vector-icons/FontAwesome';
import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: 'blue' }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color }) => <FontAwesome size={28} name="home" color={color} />,
        }}
      />
      <Tabs.Screen
        name="settings"
        options={{
          title: 'Settings',
          tabBarIcon: ({ color }) => <FontAwesome size={28} name="cog" color={color} />,
        }}
      />
    </Tabs>
  );
}

Finally, you have the two tab files that make up the content of the tabs: src/app/(tabs)/index.tsx and src/app/(tabs)/settings.tsx.

import { View, Text, StyleSheet } from 'react-native';

export default function Tab() {
  return (
    <View style={styles.container}>
      <Text>Tab [Home|Settings]</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

The tab file named index.tsx is the default tab when the app loads. The second tab file settings.tsx shows how you can add more tabs to the tab bar.

Tab bar options

The JavaScript tabs in Expo Router extend the Bottom Tabs Navigator from React Navigation.

  • The specific APIs available depend on your versions.
  • For example, Expo Router v6 extends Bottom Tabs Navigator v7.
  • Check your versions to ensure compatibility, then you can use the same configuration props to customize the bottom tab bar and individual tabs.
  • For example:
import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={
        {
          // Here to apply for all tabs
        }
      }>
      <Tabs.Screen
        name="index"
        options={
          {
            // Or here to apply for one tab
          }
        }
      />
    </Tabs>
  );
}

The supported tab bar options are listed below:

Tab bar options
OptionPlatformDescription
tabBarAccessibilityLabelAndroid, iOSAccessibility label for the tab button. This is read by the screen reader when the user taps the tab. It's recommended to set this if you don't have a label for the tab.
tabBarActiveBackgroundColorAndroid, iOSBackground color for the active tab.
tabBarActiveTintColorAndroid, iOSColor for the icon and label in the active tab.
tabBarBackgroundAndroid, iOSFunction which returns a React Element to use as background for the tab bar. You could render an image, a gradient, blur view etc.:
import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

// ...

createBottomTabNavigator({
  screenOptions: {
    tabBarStyle: { position: 'absolute' },
    tabBarBackground: () => (
      <BlurView tint="light" intensity={100} style={StyleSheet.absoluteFill} />
    ),
  },
  screens: {
    // ...
  },
});

.

import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';

// ...

<Tab.Navigator
  screenOptions={{
    tabBarStyle: { position: 'absolute' },
    tabBarBackground: () => (
      <BlurView tint="light" intensity={100} style={StyleSheet.absoluteFill} />
    ),
  }}
>

.

  • When using BlurView, make sure to set position: 'absolute' in tabBarStyle as well.
  • You'd also need to use useBottomTabBarHeight to add bottom padding to your content. | | tabBarBadge | Text to show in a badge on the tab icon.
  • Accepts a string or a number. | | tabBarBadgeStyle | Style for the badge on the tab icon.
  • You can specify a background color or text color here. | | tabBarButton | Function which returns a React element to render as the tab bar button.
  • It wraps the icon and label.
  • Renders PlatformPressable by default.
  • You can specify a custom implementation here: |
tabBarButton: (props) => <TouchableOpacity {...props} />;

| | tabBarButtonTestID | ID to locate this tab button in tests. | | tabBarHideOnKeyboard | Whether the tab bar is hidden when the keyboard opens.

  • Defaults to false. | | tabBarIcon | Function that given { focused: boolean, color: string, size: number } returns a React.Node, to display in the tab bar. | | tabBarIconStyle | Style object for the tab icon. | | tabBarInactiveBackgroundColor | Background color for the inactive tabs. | | tabBarInactiveTintColor | Color for the icon and label in the inactive tabs. | | tabBarItemStyle | Style object for the tab item container. | | tabBarLabel | Title string of a tab displayed in the tab bar or a function that given { focused: boolean, color: string } returns a React.Node, to display in tab bar.

  • When undefined, scene title is used.

  • To hide, see tabBarShowLabel. | | tabBarLabelPosition | Whether the label is shown below the icon or beside the icon.

  • By default, the position is chosen automatically based on device width|

  • . below-icon . The label is shown below the icon (typical for devices with smaller widths such as phones)

  • . beside-icon . The label is shown next to the icon (typical for larger devices such as tablets)

| | tabBarLabelStyle | Style object for the tab label. | | tabBarPosition | Position of the tab bar. Available values are|

  • bottom (Default)
  • top
  • left
  • right

.

  • When the tab bar is positioned on the left or right, it is styled as a sidebar.
  • This can be useful when you want to show a sidebar on larger screens and a bottom tab bar on smaller screens: .
  • You can also render a compact sidebar by placing the label below the icon.
  • This is only supported when the tabBarVariant is set to material: | | tabBarShowLabel | Whether the tab label should be visible.
  • Defaults to true. | | tabBarStyle | Style object for the tab bar.
  • You can configure styles such as background color here.
  • To show your screen under the tab bar, you can set the position style to absolute: |
createBottomTabNavigator({
  screenOptions: {
    tabBarStyle: { position: 'absolute' },
  },
  screens: {
    // ...
  },
});

.

<Tab.Navigator
  screenOptions={{
    tabBarStyle: { position: 'absolute' },
  }}
>

.

  • You also might need to add a bottom margin to your content if you have an absolutely positioned tab bar.

  • React Navigation won't do it automatically.

  • See useBottomTabBarHeight for more details. | | tabBarVariant | Variant of the tab bar.

  • Available values are|

  • uikit (Default) - The tab bar will be styled according to the iOS UIKit guidelines.

  • material - The tab bar will be styled according to the Material Design guidelines.

. The material variant is currently only supported when the tabBarPosition is set to left or right. |

For additional details and navigator-specific examples, see React Navigation's Bottom Tabs Navigator documentation.

Advanced

Hiding a tab

Sometimes you want a route to exist but not show up in the tab bar. You can pass href: null to disable the button:

import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen
        name="index"
        options={{
          href: null,
        }}
      />
    </Tabs>
  );
}

Dynamic routes

You can use a dynamic route in a tab bar. For example, you have a [user] tab that shows a user's profile. You can use the href option to link to a specific user's profile.

import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen
        // Name of the dynamic route.
        name="[user]"
        options={{
          // Ensure the tab always links to the same href.
          href: '/evanbacon',
          // OR you can use the href object.
          href: {
            pathname: '/[user]',
            params: {
              user: 'evanbacon',
            },
          },
        }}
      />
    </Tabs>
  );
}

Note: When adding a dynamic route in your tab layout, ensure that the dynamic route defined is unique. You cannot have two screens for the same dynamic route. For example, you cannot have two [user] tabs. If you need to have multiple dynamic routes, create a custom navigator.

Custom tab layouts

This is an experimental feature.

Expo Router offers a set of components to create custom tab layouts via the submodule 0.

  • Unlike the React Navigation styled Tabs, these components are unstyled and flexible.
  • They are designed to allow you build complex UI patterns from scratch in your project.

For other tab layouts see:

Native tabs — See native tabs if you want to achieve a native look and feel for your tab bar.

JavaScript tabs — See JavaScript tabs if you already use React Navigation's tabs.

Anatomy of custom Tabs components

There are four components offered by expo-router/ui to create custom tab layouts:

ComponentDescription
TabsWrapper component which contains the <View> for the tabs.
TabListThe containing <View> for the list of TabTrigger components.
TabTriggerA trigger component to switch to the specified tab. It is used to define the route using href prop and a name for each tab.
TabSlotA slot to render the currently selected tab.

A bare minimum structure of a custom tab layout would consist of a TabList (containing TabTrigger components for each tab) and aTabSlot, all within the Tabs component, as shown here:

import { Tabs, TabList, TabTrigger, TabSlot } from 'expo-router/ui';
import { Text } from 'react-native';

// Defining the layout of the custom tab navigator
export default function Layout() {
  return (
    <Tabs>
      <TabSlot />
      <TabList>
        <TabTrigger name="home" href="/">
          <Text>Home</Text>
        </TabTrigger>
        <TabTrigger name="article" href="/article">
          <Text>Article</Text>
        </TabTrigger>
      </TabList>
    </Tabs>
  );
}

Creating routes

The TabList contains all the routes available within the tab navigator. It must be an immediate child of Tabs. Each route is defined by a TabTrigger within the TabList. A TabTrigger within a TabList must include a name and a href prop.

Typically, the TabList defines both the available tab routes and the appearance of the tabs, with the children of each TabTrigger defining the appearance of each tab button.

Note: A name can be any string. This is a user-defined name for the Tab.

Dynamic routes

Dynamic routes are allowed and can be provided with values via the href.

_layout.tsx

[slug].tsx

The trigger <TabTrigger name="dynamic page" href="/hello-world" /> will create a tab for [slug].tsx with the params { slug: 'hello-world' }. This setup can be useful for displaying an arbitrary number of tabs in the tab bar, based on end-user data, such as showing a separate tab for each user profile in an app.

Ambiguous routes

  • _layout.tsx
  • (one,two)
    • route.tsxA route within a shared group

The href values provided to TabTrigger must always point to a single route.

  • In the above example of a shared route, href /route is not allowed, as it could refer to either /(one)/route or /(two)/route.
  • However, specifying the route group within the href would work (for example,href="/(one)/route").

Nested routes

  • _layout.tsx
  • (stack-one)
    • _layout.tsxA \<Stack> layout
    • (stack-two)
      • _layout.tsxNested \<Stack> layout
      • route.tsx

A TabTrigger can link to a deeply nested route. <TabTrigger name="route" href="/route" /> will show the (stack-one)/(stack-two)/route.tsx route.

  • This tab will be controlled by that route's parent navigator (that is, the navigator within stack-two_layout.tsx).
  • This navigation is similar to a deep link.

Rendering routes

The TabSlot component renders the current route. TabSlot can be nested inside other components within Tabs but cannot be within the TabList.

<Tabs>
  <TabList>
    <TabTrigger name="home" href="/">
      <Text>Home</Text>
    </TabTrigger>
  </TabList>
  {/* Customize how `<TabSlot />` is rendered. */}
  <View>
    <View>
      <TabSlot />
    </View>
  </View>
</Tabs>

Switching tabs

Tabs can be switched via a Link or using the imperative APIs.

  • However, these APIs will always perform a navigation action (they will switch tabs and might change the URL).
  • To switch tabs without performing any navigation, you should use a TabTrigger.
  • A TabTrigger is an unstyled <View> that will switch tabs when pressed, much like how text and components can be wrapped in Link to make them pressable navigation elements.

Resetting navigation

The reset prop from TabTrigger can be used to control when a tab resets its navigation state.

  • The options are always, onLongPress and never.
  • This is particularly useful for a stack navigator nested inside a tab.
  • For example, <TabTrigger name="home" reset="always" /> will return the user to the index route inside a tab's nested stack navigator.

TabTrigger

The TabTrigger is used to switch tabs, but also has a dual role of defining what routes are available as a tab.

Within TabList

When a TabTrigger is used as a child of TabList, that defines what routes are available within the tab navigator.

  • These TabTrigger need to include both the name and href props, as they define the URL for that tab and a custom name that can be used to refer to the tab.
  • If the TabTrigger components also contain text or other components as children, then those will also render as the tab buttons.
  • However, you can define the TabTrigger's within the TabList without any UI, and they can then be invoked by TabTrigger's outside of the TabList.

Outside TabList

An additional TabTrigger can be defined outside of a TabList, allowing you to perform the same action as the TabTrigger that is defined in the TabList.

  • In this case, the TabTrigger will not have an href prop.
  • Rather, it will perform the same action as the primary TabTrigger with the same name prop.
  • This allows you to create components that can switch tabs and be agnostic to your current navigation state.
  • Note that all TabTrigger's need to at least be descendants of the Tabs component, or else they will be considered to be outside the tab navigator and unable to invoke it.

Customizing appearance

All components are rendered unstyled as a <View>, except TabTrigger which renders as a <Pressable>.

  • This allows you to provide a custom style prop to customize their appearance.
  • Styling TabList is similar to customizing the tab bar in React Navigation, while styling TabTrigger affects the appearance of tab buttons.

If you need to change the structure of a component, you can override its underlying component by using the asChild props. The component then acts as a slot, and will forward its props to its immediate child.

<Tabs>
  <TabSlot />
  <TabList asChild>
    {/* Render a custom TabList */}
    <CustomTabList>
      <TabTrigger name="home" href="/">
        <Text>Home</Text>
      </TabTrigger>
    </CustomTabList>
  </TabList>
</Tabs>
<Tabs>
  <TabSlot />
  <TabList asChild>
    <TabTrigger name="home" href="/" asChild>
      {/* Render a custom button */}
      <CustomButton>
        <Text>Home</Text>
      </CustomButton>
    </TabTrigger>
  </TabList>
</Tabs>

Multiple tab bars

The TabList is both the configuration and default appearance of the Tabs, but it is not the only way to render a tab bar. By hiding the TabList, you can construct custom tab bars using TabTrigger.

<Tabs>
  <TabSlot />
  {/* A custom tab bar */}
  <View>
    <View>
      <TabTrigger name="home">
        <Text>Home</Text>
      </TabTrigger>
      <TabTrigger name="article">
        <Text>article</Text>
      </TabTrigger>
    </View>
  </View>
  <TabList style={{ display: 'none' }}>
    <TabTrigger name="home" href="/">
      <Text>Home</Text>
    </TabTrigger>
    <TabTrigger name="article" href="/article">
      <Text>article</Text>
    </TabTrigger>
  </TabList>
</Tabs>

TabTrigger will forward an isFocused prop, so you can create a separate tab button component that reacts to focused status.

import FontAwesome from '@expo/vector-icons/FontAwesome';
import { TabTriggerSlotProps } from 'expo-router/ui';
import { ComponentProps, Ref } from 'react';
import { Text, Pressable, View } from 'react-native';

type Icon = ComponentProps<typeof FontAwesome>['name'];

export type TabButtonProps = TabTriggerSlotProps & {
  icon?: Icon;
  ref: Ref<View>;
};

export function TabButton({ icon, children, isFocused, ...props }: TabButtonProps) {
  return (
    <Pressable
      {...props}
      style={[
        {
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          flexDirection: 'column',
          gap: 5,
          padding: 10,
        },
        isFocused ? { backgroundColor: 'white' } : undefined,
      ]}>
      <FontAwesome name={icon} />
      <Text style={[{ fontSize: 16 }, isFocused ? { color: 'white' } : undefined]}>{children}</Text>
    </Pressable>
  );
}

Hooks

All components also have a hook version giving you control over the render tree. See the Router UI Reference for a full list of the hooks available.

Using hooks is considered advanced usage of this library. For most use-cases, using the components with asChild should give you enough control over the render tree.

If you are developing a custom <TabTrigger />, you may also need to develop a custom <TabList /> as <TabList /> uses the useTabsWithChildren() which requires using the exported <TabTrigger /> component.

Customizing how tab screens are rendered

The TabSlot accepts a renderFn property.

  • This function can be used to override how your screen is rendered, allowing you to implement advanced functionality such as animations or persisting/unmounting screens.
  • See the Router UI Reference for more information.

Common questions

How do I create multiple tabs for the same route?
  • _layout.tsxTabs layout
  • (movie,tv)
    • [id].tsx

You should add the route to a shared group and create a separate TabTrigger for each group group.

How do I hide a tab?

Not rendering the TabTrigger will remove that tab (and its navigation state) from your app.

How do I create animated tabs?

You can provide a custom renderer to TabSlot to customize how it renders a screen. You can use this to detect when screen is focused and animate appropriately.

Can I use relative hrefs?
  • directory
    • _layout.tsxThe local pathname is /directory
    • page.tsxThe pathname is /directory/page
    • profile.tsxThe pathname is /directory/profile

A TabTrigger with a relative href is relative to the local path name Tabs was rendered on.

  • This is different from normal relative hrefs which are relative to the current displayed route.
  • For example, the <TabTrigger href="./profile" /> will resolve to /directory/profile, even when the /directory/page route is showing.
  • Expo recommends against using relative hrefs.

expo-router/ui API

expo-router/ui is a submodule of expo-router library and exports components and hooks to build custom tab layouts, rather than using the default React Navigation navigators provided by expo-router.

Installation

To use expo-router/ui in your project, you need to install expo-router in your project. Follow the instructions from the Expo Router's installation guide:

Install Expo Router — Learn how to install Expo Router in your project.

Configuration in app config

If you are using the default template to create a new project, expo-router's config plugin is already configured in your app config.

Example app.json with config plugin

{
  "expo": {
    "plugins": ["expo-router"]
  }
}

Usage

For information about using expo-router/ui in Custom tab layouts guide:

Custom tab layouts

API

import { Tabs, TabList, TabTrigger, TabSlot } from 'expo-router/ui';

Components

TabContext

Type: React.Element<Context<ExpoTabsNavigatorScreenOptions>>

TabList

Type: React.Element<TabListProps>

Wrapper component for TabTriggers. TabTriggers within the TabList define the tabs.

Example

<Tabs>
 <TabSlot />
 <TabList>
  <TabTrigger name="home" href="/" />
 </TabList>
</Tabs>

TabListProps

asChild

Optional • Type: boolean

Forward props to child component and removes the extra <View>. Useful for custom wrappers.

Inherited props

Tabs

Type: React.Element<TabsProps>

Root component for the headless tabs.

See: useTabsWithChildren for a hook version of this component.

Example

<Tabs>
 <TabSlot />
 <TabList>
  <TabTrigger name="home" href="/" />
 </TabList>
</Tabs>

TabsProps

asChild

Optional • Type: boolean

Forward props to child component and removes the extra <View>. Useful for custom wrappers.

options

Optional • Type: UseTabsOptions

Inherited props

TabSlot

Type: React.Element<TabSlotProps>

Renders the current tab.

See: useTabSlot for a hook version of this component.

Example

<Tabs>
 <TabSlot />
 <TabList>
  <TabTrigger name="home" href="/" />
 </TabList>
</Tabs>

TabSlotProps

detachInactiveScreens

Optional • Type: boolean

Remove inactive screens.

renderFn

Optional • Type: defaultTabsSlotRender

Override how the Screen component is rendered.

Inherited props
  • ComponentProps<ScreenContainer>

TabTrigger

Type: React.Element<TabTriggerProps>

Creates a trigger to navigate to a tab. When used as child of TabList, its functionality slightly changes since the href prop is required, and the trigger also defines what routes are present in the Tabs.

When used outside of TabList, this component no longer requires an href.

Example

<Tabs>
 <TabSlot />
 <TabList>
  <TabTrigger name="home" href="/" />
 </TabList>
</Tabs>

TabTriggerProps

asChild

Optional • Type: boolean

Forward props to child component. Useful for custom wrappers.

href

Optional • Type: Href

Name of tab. Required when used within a TabList.

name

Type: string

Name of tab. When used within a TabList this sets the name of the tab. Otherwise, this references the name.

resetOnFocus

Optional • Type: boolean

Resets the route when switching to a tab.

Inherited props
  • PressablePropsWithoutFunctionChildren

useTabSlot

Type: React.Element<TabSlotProps>

Returns a ReactElement of the current tab.

Example

function MyTabSlot() {
  const slot = useTabSlot();

  return slot;
}

Hooks

useTabSlot(namedParameters)

ParameterType
namedParameters(optional)TabSlotProps

Returns a ReactElement of the current tab.

Returns: Element

Example

function MyTabSlot() {
  const slot = useTabSlot();

  return slot;
}

useTabsWithChildren(options)

ParameterType
optionsUseTabsWithChildrenOptions

Hook version of Tabs. The returned NavigationContent component should be rendered. Using the hook requires using the <TabList /> and <TabTrigger /> components exported from Expo Router.

The useTabsWithTriggers() hook can be used for custom components.

Returns: { describe: (route: RouteProp<ParamListBase>, placeholder: boolean) => Descriptor<ExpoTabsNavigatorScreenOptions, Omit<NavigationHelpersCommon<ParamListBase, TabNavigationState<any>>, 'getParent'> & { } & NavigationHelpersRoute<ParamListBase, string> & EventConsumer<TabNavigationEventMap & EventMapCore<TabNavigationState<any>>> & PrivateValueStore<[ParamListBase, string, TabNavigationEventMap]> & TabActionHelpers<ParamListBase>, RouteProp<ParamListBase>>, descriptors: Record<string, Descriptor<ExpoTabsNavigatorScreenOptions, Omit<NavigationHelpersCommon<ParamListBase, TabNavigationState<any>>, 'getParent'> & { } & NavigationHelpersRoute<ParamListBase, string> & EventConsumer<TabNavigationEventMap & EventMapCore<TabNavigationState<any>>> & PrivateValueStore<[ParamListBase, string, TabNavigationEventMap]> & TabActionHelpers<ParamListBase>, RouteProp<ParamListBase>>>, navigation: { } & PrivateValueStore<[ParamListBase, unknown, unknown]> & EventEmitter<TabNavigationEventMap> & NavigationHelpersRoute<ParamListBase, string> & TabActionHelpers<ParamListBase>, NavigationContent: (__namedParameters: { children: ReactNode }) => Element, state: TabNavigationState<any> }

See: Tabs for the component version of this hook.

Example

export function MyTabs({ children }) {
 const { NavigationContent } = useTabsWithChildren({ children })

 return <NavigationContent />
}

useTabsWithTriggers(options)

ParameterType
optionsUseTabsWithTriggersOptions

Alternative hook version of Tabs that uses explicit triggers instead of children.

Returns: { describe: (route: RouteProp<ParamListBase>, placeholder: boolean) => Descriptor<ExpoTabsNavigatorScreenOptions, Omit<NavigationHelpersCommon<ParamListBase, TabNavigationState<any>>, 'getParent'> & { } & NavigationHelpersRoute<ParamListBase, string> & EventConsumer<TabNavigationEventMap & EventMapCore<TabNavigationState<any>>> & PrivateValueStore<[ParamListBase, string, TabNavigationEventMap]> & TabActionHelpers<ParamListBase>, RouteProp<ParamListBase>>, descriptors: Record<string, Descriptor<ExpoTabsNavigatorScreenOptions, Omit<NavigationHelpersCommon<ParamListBase, TabNavigationState<any>>, 'getParent'> & { } & NavigationHelpersRoute<ParamListBase, string> & EventConsumer<TabNavigationEventMap & EventMapCore<TabNavigationState<any>>> & PrivateValueStore<[ParamListBase, string, TabNavigationEventMap]> & TabActionHelpers<ParamListBase>, RouteProp<ParamListBase>>>, navigation: { } & PrivateValueStore<[ParamListBase, unknown, unknown]> & EventEmitter<TabNavigationEventMap> & NavigationHelpersRoute<ParamListBase, string> & TabActionHelpers<ParamListBase>, NavigationContent: (__namedParameters: { children: ReactNode }) => Element, state: TabNavigationState<any> }

See: Tabs for the component version of this hook.

Example

export function MyTabs({ children }) {
  const { NavigationContent } = useTabsWithChildren({ triggers: [] })

  return <NavigationContent />
}

useTabTrigger(options)

ParameterType
optionsTabTriggerProps

Utility hook creating custom TabTrigger.

Returns: UseTabTriggerResult

Types

ExpoTabsNavigationProp

Type: NavigationProp<ParamList, RouteName, NavigatorID, TabNavigationState<ParamListBase>, ExpoTabsScreenOptions, TabNavigationEventMap>

ExpoTabsNavigatorOptions

Literal type: union

Acceptable values are: DefaultNavigatorOptions<ParamListBase, string | undefined, TabNavigationState<ParamListBase>, ExpoTabsScreenOptions, TabNavigationEventMap, ExpoTabsNavigationProp<ParamListBase>> | Omit<TabRouterOptions, 'initialRouteName'> | ExpoTabsNavigatorScreenOptions

ExpoTabsNavigatorScreenOptions

PropertyTypeDescription
detachInactiveScreens(optional)boolean-
freezeOnBlur(optional)boolean-
lazy(optional)boolean-
unmountOnBlur(optional)boolean-

ExpoTabsScreenOptions

Type: Pick<BottomTabNavigationOptions, 'title' | 'lazy' | 'freezeOnBlur'> extended by:

PropertyTypeDescription
actionNavigationAction-
params(optional)object-
titlestring-

SwitchToOptions

Options for switchTab function.

PropertyTypeDescription
resetOnFocus(optional)booleanNavigate and reset the history on route focus.

TabNavigationEventMap

PropertyTypeDescription
tabLongPress{ data: undefined }Event which fires on long press on the tab in the tab bar.
tabPress{ canPreventDefault: true, data: undefined }Event which fires on tapping on the tab in the tab bar.

TabsContextValue

Type: ReturnType<useNavigationBuilder>

The React Navigation custom navigator.

See: useNavigationBuilder hook from React Navigation for more information.

TabsSlotRenderOptions

Options provided to the UseTabSlotOptions.

PropertyTypeDescription
detachInactiveScreensbooleanShould the screen be unloaded when inactive.
indexnumberIndex of screen.
isFocusedbooleanWhether the screen is focused.
loadedbooleanWhether the screen has been loaded.

TabTriggerOptions

PropertyTypeDescription
hrefHref-
namestring-

Trigger

Type: extended by:

PropertyTypeDescription
isFocusedboolean-
resolvedHrefstring-
route[number]-

UseTabsOptions

Options to provide to the Tab Router.

Type: Omit<DefaultNavigatorOptions<ParamListBase, any, TabNavigationState<any>, ExpoTabsScreenOptions, TabNavigationEventMap, any>, 'children'> extended by:

PropertyTypeDescription
backBehavior(optional)TabRouterOptions[backBehavior]-

UseTabsWithChildrenOptions

Type: PropsWithChildren<UseTabsOptions>

UseTabsWithTriggersOptions

Type: UseTabsOptions extended by:

PropertyTypeDescription
triggersScreenTrigger[]-

UseTabTriggerResult

PropertyTypeDescription
getTrigger(name: string) => Trigger | undefined-
switchTab(name: string, options: SwitchToOptions) => void-
trigger(optional)Trigger-
triggerPropsTriggerProps-

Sources

The original Expo docs for this page:

On this page

Get startedCustomizing tab bar itemsIconIcon rendering modeAsset catalog icons (iOS)Vector iconsLabelBadgeCustomizing the tab barAdvancedHiding the Tab barHiding a tab conditionallyDismiss behaviorScroll to topDisabled tabsiOS 26 featuresSeparate search tabTabbar search inputTab bar minimize behaviorBottom accessorySafe area handlingDisabling automatic content insetsLazy loadingRender content only when focusedLoad once on first focusMigrating from JavaScript tabsUse Trigger instead of ScreenUse React components instead of propsUse Stacks inside tabsCommon problemsThe tab bar is transparent on iOS 18 and earlierWhite background flashes when switching tabs on iOS 26The tab bar background props have no effect on iOS 26Scroll to top does not work when tapping a tabLiquid glass header buttons flicker in dark mode on iOS 26Known limitationsThe default and selected icons share one rendering mode on iOSCannot measure the tab bar heightNo support for nested native tabsLimited support for FlatListNo support for dynamically adding or removing tabsNative tabs APIInstallationConfiguration in app configExample app.json with config pluginUsageAPIComponentsNativeTabsbackgroundColorbadgeBackgroundColorblurEffectdisableTransparentOnScrollEdgehiddeniconColorlabelStyleminimizeBehaviorscreenListenersshadowColorsidebarAdaptabletintColortitlePositionAdjustmentunstable_nativePropsInherited propsNativeTabs.TriggerchildrencontentStyledisableAutomaticContentInsetsdisableddisablePopToTopdisableScrollToTopdisableTransparentOnScrollEdgehiddenlistenersnameroleunstable_nativePropsNativeTabs.BottomAccessorychildrenNativeTabs.Trigger.BadgechildrenhiddenselectedBackgroundColorNativeTabs.Trigger.IconselectedColorNativeTabs.Trigger.LabelchildrenhiddenselectedStyleNativeTabs.Trigger.VectorIconInterfacesDrawableIconSFSymbolIconSrcIconXcassetIconTypesNativeTabsLabelStyleSymbolOrImageSourceJavaScript tabsMultiple tab layoutsGet started with JavaScript tabsTab bar optionsTab bar optionsAdvancedHiding a tabDynamic routesCustom tab layoutsAnatomy of custom Tabs componentsCreating routesDynamic routesAmbiguous routesNested routesRendering routesSwitching tabsResetting navigationTabTriggerWithin TabListOutside TabListCustomizing appearanceMultiple tab barsHooksCustomizing how tab screens are renderedCommon questionsHow do I create multiple tabs for the same route?How do I hide a tab?How do I create animated tabs?Can I use relative hrefs?expo-router/ui APIInstallationConfiguration in app configExample app.json with config pluginUsageAPIComponentsTabContextTabListasChildInherited propsTabsasChildoptionsInherited propsTabSlotdetachInactiveScreensrenderFnInherited propsTabTriggerasChildhrefnameresetOnFocusInherited propsuseTabSlotHooksuseTabSlot(namedParameters)useTabsWithChildren(options)useTabsWithTriggers(options)useTabTrigger(options)TypesExpoTabsNavigationPropExpoTabsNavigatorOptionsExpoTabsNavigatorScreenOptionsExpoTabsScreenOptionsSwitchToOptionsTabNavigationEventMapTabsContextValueTabsSlotRenderOptionsTabTriggerOptionsTriggerUseTabsOptionsUseTabsWithChildrenOptionsUseTabsWithTriggersOptionsUseTabTriggerResultSources