Custom tab layouts

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.

Anatomy of custom Tabs components

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

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

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

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

Creating routes

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

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

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

Dynamic routes

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

_layout.tsx
[slug].tsx

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

Ambiguous routes

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

The href values provided to TabTrigger must always point to a single route. In the above example of a shared route, href /route is not allowed, as it could refer to either /(one)/route or /(two)/route. However, specifying the route group within the href would work (for example,href="/(one)/route").

Nested routes

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

A TabTrigger can link to a deeply nested route. <TabTrigger name="route" href="/route" /> will show the (stack-one)/(stack-two)/route.tsx route. This tab will be controlled by that route's parent navigator (that is, the navigator within stack-two_layout.tsx). This navigation is similar to a deep link.

Rendering routes

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

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

Switching tabs

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

Resetting navigation

The reset prop from TabTrigger can be used to control when a tab resets its navigation state. The options are always, onLongPress and never. This is particularly useful for a stack navigator nested inside a tab. For example, <Trigger name="home" reset="always" /> will return the user to the index route inside a tab's nested stack navigator.

TabTrigger

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

Within TabList

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

Outside TabList

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

Customizing appearance

All components are rendered unstyled as a <View>, except TabTrigger which renders as a <Pressable>. This allows you to provide a custom style prop to customize their appearance. Styling TabList is similar to customizing the tab bar in React Navigation, while styling TabTrigger affects the appearance of tab buttons.

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

Custom TabList
<Tabs>
  <TabSlot />
  <TabList asChild>
    {/* Render a custom TabList */}
    <CustomTabList>
      <TabTrigger name="home" href="/">
        <Text>Home</Text>
      </TabTrigger>
    </CustomTabList>
  </TabList>
</Tabs>
Custom Button
<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.

Multiple tab bars example
<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.

TabButton.tsx
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>
    );
  }
);

Hooks

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.

Customizing how tab screens are rendered

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.

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