Native tabs

Edit this page

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


Native tabs are an experimental feature available in SDK 54 and above, and their 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:

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 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.

app/_layout.tsx
import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <Label>Home</Label> <Icon sf="house.fill" drawable="custom_android_drawable" /> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Icon sf="gear" drawable="custom_settings_drawable" /> <Label>Settings</Label> </NativeTabs.Trigger> </NativeTabs> ); }

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

app/index.tsx and app/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.

In contrast to the Stack navigator, tabs are not automatically added to the tab bar. You need to explicitly add them in your layout file using the NativeTabs.Trigger.

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

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

Additionally, you can use the selectedSf or selectedSrc props to specify a different icon when the tab is selected.

The custom images API is currently not available on Android, but we plan to add it to soon.

To use drawable props on Android, you can use built-in drawables or add custom drawables.

app/_layout.tsx
import { NativeTabs, Icon } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <Icon sf="house" selectedSf="house.fill" drawable="custom_home_drawable" /> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Icon src={require('../../../assets/setting_icon.png')} selectedSrc={require('../../../assets/selected_setting_icon.png')} /> </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.

app/_layout.tsx
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <Label>Home</Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <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.

app/_layout.tsx
import { NativeTabs, Badge } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="messages"> <Badge>9+</Badge> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <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 a tab conditionally

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

app/_layout.tsx
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> ); }

Dismiss behavior

Currently this is an iOS-only feature, but we plan to add it to Android in the future.

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.

app/_layout.tsx
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index" disablePopToTop> <Label>Home</Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Label>Settings</Label> </NativeTabs.Trigger> </NativeTabs> ); }

Scroll to top

Currently this is an iOS-only feature, but we plan to add it to Android in the future.

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.

app/_layout.tsx
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index" disableScrollToTop> <Label>Home</Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Label>Settings</Label> </NativeTabs.Trigger> </NativeTabs> ); }

Known limitations

A limit of 5 tabs on Android

On Android, there is a limitation of having a maximum of 5 tabs in the tab bar. This restriction comes from the platform's Material Tabs component.