Edit this page
Learn how to use headless tab components to create custom tab layouts in Expo Router.
Experimentally available in SDK 52 and above. For the React Navigation styled tabs layout that are commonly used in native apps, see Tabs.
Expo Router offers a set of components to create custom tab layouts via the submodule expo-router/ui
. 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.
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';
// Defining the layout of the custom tab navigator
export 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>
);
}
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 anystring
. This is a user-defined name for the Tab.
Dynamic routes are allowed and can be provided with values via the href
.
_layout.tsx
[slug].tsx
The trigger <Trigger 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.
_layout.tsx
(one,two)
route.tsx
A 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"
).
_layout.tsx
(stack-one)
_layout.tsx
A <Stack> layout
(stack-two)
_layout.tsx
Nested <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.
The TabSlot
component renders the current route. TabSlot
be 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>
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.
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, <Trigger name="home" reset="always" />
will return the user to the index route inside a tab's nested stack navigator.
The TabTrigger
is used to switch tabs, but also has a dual role of defining what routes are available as a tab.
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
.
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.
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>
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, forwardRef } from 'react';
import { Text, Pressable, View } from 'react-native';
type Icon = ComponentProps<typeof FontAwesome>['name'];
export type TabButtonProps = TabTriggerSlotProps & {
icon?: Icon;
};
export const TabButton = forwardRef(
({ icon, children, isFocused, ...props }: TabButtonProps, ref: Ref<View>) => {
return (
<Pressable
ref={ref}
{...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>
);
}
);
All components also have a hook version and allows to you create the same functionality without the restrictions of using components. Using hooks, you have complete control over the render tree. See the Router UI Reference for a full list of the hooks available.
The TabSlot
accepts an options
prop with 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.
_layout.tsx
Tabs layout
(movie,tv)
[id].tsx
You should add the route to a shared group and create a separate TabTrigger
for each group group
.
Not rendering the TabTrigger
will remove that tab (and its navigation state) from your app.
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 an animate appropriately.
directory
_layout.tsx
The local pathname is /directory
page.tsx
The pathname is /directory/page
profile.tsx
The pathname is /directory/page
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.