Expo UI and Router for iOS
Expo Router

Custom tab layouts

Learn how to use headless tab components to create custom tab layouts in Expo Router.

This is an experimental feature.

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.

For other tab layouts see:

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

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

Anatomy of custom Tabs components

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

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

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

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

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

Creating routes

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

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

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

Dynamic routes

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

_layout.tsx

[slug].tsx

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

Ambiguous routes

_layout.tsx

(one,two)

route.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").

Nested routes

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

Rendering routes

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

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

Switching tabs

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

Resetting navigation

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

TabTrigger

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

Within TabList

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

Outside TabList

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

Customizing appearance

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

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

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

Multiple tab bars

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

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

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

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

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

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

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

Hooks

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

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

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

Customizing how tab screens are rendered

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

Common questions

How do I create multiple tabs for the same route?

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

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.tsx``The local pathname is /directory

page.tsx``The pathname is /directory/page

profile.tsx``The pathname is /directory/profile

A TabTrigger with a relative href is relative to the local path name Tabs was rendered on. This is different from normal relative hrefs which are relative to the current displayed route. For example, the <TabTrigger href="./profile" /> will resolve to /directory/profile, even when the /directory/page route is showing. Expo recommends against using relative hrefs.

API reference

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

Installation

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

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

Configuration in app config

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

Example app.json with config plugin

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

Usage

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

Custom tab layouts

API

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

Components

TabContext

Type: React.Element<Context<ExpoTabsNavigatorScreenOptions>>

TabList

Type: React.Element<TabListProps>

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

Example

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

TabListProps

asChild

Optional • Type: boolean

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

Inherited props

Tabs

Type: React.Element<TabsProps>

Root component for the headless tabs.

See: useTabsWithChildren for a hook version of this component.

Example

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

TabsProps

asChild

Optional • Type: boolean

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

options

Optional • Type: UseTabsOptions

Inherited props

TabSlot

Type: React.Element<TabSlotProps>

Renders the current tab.

See: useTabSlot for a hook version of this component.

Example

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

TabSlotProps

detachInactiveScreens

Optional • Type: boolean

Remove inactive screens.

renderFn

Optional • Type: defaultTabsSlotRender

Override how the Screen component is rendered.

Inherited props
  • ComponentProps<ScreenContainer>

TabTrigger

Type: React.Element<TabTriggerProps>

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

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

Example

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

TabTriggerProps

asChild

Optional • Type: boolean

Forward props to child component. Useful for custom wrappers.

href

Optional • Type: Href

Name of tab. Required when used within a TabList.

name

Type: string

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

resetOnFocus

Optional • Type: boolean

Resets the route when switching to a tab.

Inherited props
  • PressablePropsWithoutFunctionChildren

useTabSlot

Type: React.Element<TabSlotProps>

Returns a ReactElement of the current tab.

Example

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

  return slot;
}

Hooks

useTabSlot(namedParameters)

ParameterType
namedParameters(optional)TabSlotProps

Returns a ReactElement of the current tab.

Returns: Element

Example

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

  return slot;
}

useTabsWithChildren(options)

ParameterType
optionsUseTabsWithChildrenOptions

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

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

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

See: Tabs for the component version of this hook.

Example

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

 return <NavigationContent />
}

useTabsWithTriggers(options)

ParameterType
optionsUseTabsWithTriggersOptions

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

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

See: Tabs for the component version of this hook.

Example

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

  return <NavigationContent />
}

useTabTrigger(options)

ParameterType
optionsTabTriggerProps

Utility hook creating custom TabTrigger.

Returns: UseTabTriggerResult

Types

ExpoTabsNavigationProp

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

ExpoTabsNavigatorOptions

Literal type: union

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

ExpoTabsNavigatorScreenOptions

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

ExpoTabsScreenOptions

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

PropertyTypeDescription
actionNavigationAction-
params(optional)object-
titlestring-

SwitchToOptions

Options for switchTab function.

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

TabNavigationEventMap

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

TabsContextValue

Type: ReturnType<useNavigationBuilder>

The React Navigation custom navigator.

See: useNavigationBuilder hook from React Navigation for more information.

TabsSlotRenderOptions

Options provided to the UseTabSlotOptions.

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

TabTriggerOptions

PropertyTypeDescription
hrefHref-
namestring-

Trigger

Type: extended by:

PropertyTypeDescription
isFocusedboolean-
resolvedHrefstring-
route[number]-

UseTabsOptions

Options to provide to the Tab Router.

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

PropertyTypeDescription
backBehavior(optional)TabRouterOptions[backBehavior]-

UseTabsWithChildrenOptions

Type: PropsWithChildren<UseTabsOptions>

UseTabsWithTriggersOptions

Type: UseTabsOptions extended by:

PropertyTypeDescription
triggersScreenTrigger[]-

UseTabTriggerResult

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

Sources

The original Expo docs for this page:

On this page