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.Iconis available in SDK 55 and later. For SDK 54, useIconimported fromexpo-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
renderingModeprop, just likesrcicons. WheniconColoris set, icons default totemplaterendering. Otherwise, they default tooriginal.
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
getImageSourceSyncrequires 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
srcwithsfto use the vector icon on Android and SF Symbols on iOS. - On iOS,
sftakes precedence oversrc; on Android, the icon falls back tosrc.
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
hiddenproperty 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
hiddenmeans 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
disabledprop 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
hiddenif you want to remove it from the tab bar entirely.
Note:
disabledonly suppresses the native tap interaction. It is not a "protected" or authorization gate - JavaScript navigation such asrouter.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:
- iOS: The first
ScrollViewnested inside a native tabs screen has automatic content inset adjustment enabled. This ensures content scrolls correctly behind the tab bar.
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, andDefaultThemeare exported fromexpo-routerin SDK 56 and later. For SDK 55, import them from@react-navigation/nativeinstead.
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, andDefaultThemeare exported fromexpo-routerin SDK 56 and later. For SDK 55, import them from@react-navigation/nativeinstead.
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 theIconselectedColorprop without also setting a color for the default state. - A color implies
'template'rendering, while an uncolored icon defaults to'original'. - Setting
renderingModefor 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 behaviornever- the tab bar does not minimizeonScrollDown- the tab bar minimizes when scrolling down and expands when scrolling back uponScrollUp- 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 pressedfocus- called when the screen comes into focusblur- 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
hiddenmeans 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 pressedfocus- called when this screen comes into focusblur- 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
Interfaces
DrawableIcon
| Property | Type | Description |
|---|
- 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
| Property | Type | Description |
|---|---|---|
| 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
| Property | Type | Description |
|---|---|---|
| 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
sfprop is used it will override this prop on iOS. -
When
drawableormaterialprop 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
| Property | Type | Description |
|---|---|---|
| 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:
| Property | Type | Description |
|---|---|---|
| sf(optional) | SFSymbol | The name of the SF Symbol to use as an icon. |
| xcasset(optional) | string | The name of the iOS asset catalog image to use as an icon. |
Or object shaped as below:
| Property | Type | Description |
|---|---|---|
| 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/uito 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
| Option | Platform | Description |
|---|---|---|
tabBarAccessibilityLabel | Android, iOS | Accessibility 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. |
tabBarActiveBackgroundColor | Android, iOS | Background color for the active tab. |
tabBarActiveTintColor | Android, iOS | Color for the icon and label in the active tab. |
tabBarBackground | Android, iOS | Function 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 setposition: 'absolute'intabBarStyleas well. - You'd also need to use
useBottomTabBarHeightto add bottom padding to your content. | |tabBarBadge| Text to show in a badge on the tab icon. - Accepts a
stringor anumber. | |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
PlatformPressableby 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
titleis 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)topleftright
.
- When the tab bar is positioned on the
leftorright, 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
tabBarVariantis set tomaterial: | |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
positionstyle 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
useBottomTabBarHeightfor 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:
| Component | Description |
|---|---|
Tabs | Wrapper component which contains the <View> for the tabs. |
TabList | The containing <View> for the list of TabTrigger components. |
TabTrigger | A trigger component to switch to the specified tab. It is used to define the route using href prop and a name for each tab. |
TabSlot | A 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
namecan be anystring. 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
/routeis not allowed, as it could refer to either/(one)/routeor/(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
TabTriggeris an unstyled<View>that will switch tabs when pressed, much like how text and components can be wrapped inLinkto 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,onLongPressandnever. - 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
TabTriggerneed to include both thenameandhrefprops, as they define the URL for that tab and a custom name that can be used to refer to the tab. - If the
TabTriggercomponents 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 theTabListwithout any UI, and they can then be invoked byTabTrigger's outside of theTabList.
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
TabTriggerwill not have anhrefprop. - Rather, it will perform the same action as the primary
TabTriggerwith the samenameprop. - 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 theTabscomponent, 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
styleprop to customize their appearance. - Styling
TabListis similar to customizing the tab bar in React Navigation, while stylingTabTriggeraffects 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/pageroute 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:
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:
useTabsWithChildrenfor 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:
useTabSlotfor 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)
| Parameter | Type |
|---|---|
namedParameters(optional) | TabSlotProps |
Returns a ReactElement of the current tab.
Returns: Element
Example
function MyTabSlot() {
const slot = useTabSlot();
return slot;
}useTabsWithChildren(options)
| Parameter | Type |
|---|---|
options | UseTabsWithChildrenOptions |
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:
Tabsfor the component version of this hook.
Example
export function MyTabs({ children }) {
const { NavigationContent } = useTabsWithChildren({ children })
return <NavigationContent />
}useTabsWithTriggers(options)
| Parameter | Type |
|---|---|
options | UseTabsWithTriggersOptions |
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:
Tabsfor the component version of this hook.
Example
export function MyTabs({ children }) {
const { NavigationContent } = useTabsWithChildren({ triggers: [] })
return <NavigationContent />
}useTabTrigger(options)
| Parameter | Type |
|---|---|
options | TabTriggerProps |
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
| Property | Type | Description |
|---|---|---|
| detachInactiveScreens(optional) | boolean | - |
| freezeOnBlur(optional) | boolean | - |
| lazy(optional) | boolean | - |
| unmountOnBlur(optional) | boolean | - |
ExpoTabsScreenOptions
Type: Pick<BottomTabNavigationOptions, 'title' | 'lazy' | 'freezeOnBlur'> extended by:
| Property | Type | Description |
|---|---|---|
| action | NavigationAction | - |
| params(optional) | object | - |
| title | string | - |
SwitchToOptions
Options for switchTab function.
| Property | Type | Description |
|---|---|---|
| resetOnFocus(optional) | boolean | Navigate and reset the history on route focus. |
TabNavigationEventMap
| Property | Type | Description |
|---|---|---|
| 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:
useNavigationBuilderhook from React Navigation for more information.
TabsSlotRenderOptions
Options provided to the UseTabSlotOptions.
| Property | Type | Description |
|---|---|---|
| detachInactiveScreens | boolean | Should the screen be unloaded when inactive. |
| index | number | Index of screen. |
| isFocused | boolean | Whether the screen is focused. |
| loaded | boolean | Whether the screen has been loaded. |
TabTriggerOptions
| Property | Type | Description |
|---|---|---|
| href | Href | - |
| name | string | - |
Trigger
Type: extended by:
| Property | Type | Description |
|---|---|---|
| isFocused | boolean | - |
| resolvedHref | string | - |
| route | [number] | - |
UseTabsOptions
Options to provide to the Tab Router.
Type: Omit<DefaultNavigatorOptions<ParamListBase, any, TabNavigationState<any>, ExpoTabsScreenOptions, TabNavigationEventMap, any>, 'children'> extended by:
| Property | Type | Description |
|---|---|---|
| backBehavior(optional) | TabRouterOptions[backBehavior] | - |
UseTabsWithChildrenOptions
Type: PropsWithChildren<UseTabsOptions>
UseTabsWithTriggersOptions
Type: UseTabsOptions extended by:
| Property | Type | Description |
|---|---|---|
| triggers | ScreenTrigger[] | - |
UseTabTriggerResult
| Property | Type | Description |
|---|---|---|
| getTrigger | (name: string) => Trigger | undefined | - |
| switchTab | (name: string, options: SwitchToOptions) => void | - |
| trigger(optional) | Trigger | - |
| triggerProps | TriggerProps | - |
Sources
The original Expo docs for this page:
- Native tabs (
source-of-truth/router/advanced/native-tabs.md) - Router Native tabs (
source-of-truth/versions/v57.0.0/sdk/router/native-tabs.md) - JavaScript tabs (
source-of-truth/router/advanced/tabs.md) - Custom tab layouts (
source-of-truth/router/advanced/custom-tabs.md) - Router UI (
source-of-truth/versions/v57.0.0/sdk/router/ui.md)