Dynamic routes with TabTrigger
Dynamic routes are allowed in TabTrigger and can be provided with values via the href prop. For example, <TabTrigger name="dynamic page" href="/hello-world" /> will create a tab for a [slug].tsx dynamic route with params { slug: 'hello-world' }. This is useful for displaying an arbitrary number of tabs based on end-user data.
Ambiguous routes in TabTrigger href
The href values provided to TabTrigger must always point to a single route. If a route exists in multiple shared groups (e.g., /(one,two)/route.tsx), using href="/route" is not allowed as it is ambiguous. Instead, specify the route group within the href, such as href="/(one)/route".
Custom tabs components overview
Expo Router offers unstyled, flexible custom tab components via the `expo-router/ui` submodule. There are four main components: Tabs (wrapper containing the View for tabs), TabList (containing View for list of TabTrigger components), TabTrigger (trigger component to switch to specified tab using href prop and name), and TabSlot (slot to render currently selected tab).
Minimum custom tabs structure
A bare minimum custom tab layout consists of a TabList (containing TabTrigger components for each tab) and a TabSlot, all within the Tabs component. Example: <Tabs><TabSlot /><TabList><TabTrigger name="home" href="/"><Text>Home</Text></TabTrigger></TabList></Tabs>
TabTrigger required props in TabList
A TabTrigger within a TabList must include both a name prop (any string, user-defined name for the tab) and an href prop (the route path).
Nested routes with TabTrigger
A TabTrigger can link to deeply nested routes. For example, <TabTrigger name="route" href="/route" /> can show a route nested in multiple stack layouts like (stack-one)/(stack-two)/route.tsx. This navigation is similar to a deep link and will be controlled by that route's parent navigator.
TabSlot placement and nesting
The TabSlot component renders the current route. TabSlot can be nested inside other components within Tabs, but cannot be within the TabList.
Switching tabs without navigation
To switch tabs without performing any navigation action (which would change the URL), use TabTrigger instead of Link or imperative APIs. TabTrigger is an unstyled View that switches tabs when pressed without navigation side effects.
TabTrigger reset prop options
The reset prop on TabTrigger controls when a tab resets its navigation state. Options are: always (always reset), onLongPress (reset on long press), and never (never reset). This is useful for stack navigators nested inside a tab, e.g., <TabTrigger name="home" reset="always" /> returns user to index route.
TabTrigger dual role in TabList vs outside
When TabTrigger is a child of TabList, it defines available routes and must include name and href props. When TabTrigger is outside TabList, it performs the same action as the TabTrigger in TabList with the same name prop, without requiring href. All TabTriggers must be descendants of the Tabs component to work.
Custom tab components rendering
All custom tab components render unstyled as a View, except TabTrigger which renders as a Pressable. You can provide a custom style prop to customize appearance. Use the asChild prop to override the underlying component and act as a slot forwarding props to immediate child.
Multiple tab bars implementation
You can create multiple tab bars by hiding TabList (e.g., style={{ display: 'none' }}) and constructing custom tab bars using TabTrigger components outside TabList. TabTrigger components outside TabList do not need the href prop.
TabTrigger isFocused prop forwarding
TabTrigger forwards an isFocused prop, allowing you to create a custom tab button component that reacts to focused status. This can be used for styling tabs based on whether they are currently active.
Custom tab button with isFocused example
Example TabButton component using isFocused prop:
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 for custom tab control
All custom tab components have hook versions providing control over the render tree. Using hooks is considered advanced usage; for most use cases, components with asChild prop should provide sufficient control. If developing custom TabTrigger, you may need custom TabList using useTabsWithChildren().
TabSlot renderFn property
TabSlot accepts a renderFn property to override how screens are rendered, enabling advanced functionality such as animations or persisting/unmounting screens.
Multiple tabs for same route
To create multiple tabs for the same route, add the route to a shared group (e.g., (movie,tv)/[id].tsx) and create a separate TabTrigger for each group variant.
Hide tab by not rendering TabTrigger
To hide a tab, do not render the corresponding TabTrigger. This removes that tab and its navigation state from the app.
Animated tabs with TabSlot
To create animated tabs, provide a custom renderer to TabSlot using the renderFn property to customize screen rendering. Use this to detect when screen is focused and animate appropriately.
Relative href behavior in TabTrigger
A TabTrigger with relative href is relative to the local pathname where Tabs was rendered, not to the currently displayed route. For example, <TabTrigger href="./profile" /> in /directory/_layout.tsx resolves to /directory/profile even when /directory/page is showing. Expo recommends against using relative hrefs in TabTrigger.
Custom tabs experimental status
Custom tab layouts are an experimental feature in Expo Router.
Tabs.Protected for protected routes in tab navigator
Protected routes are available for Tabs navigator using Tabs.Protected with the guard prop. Wrap tab screens in Tabs.Protected to conditionally show or hide tabs based on authentication state.
File structure for JavaScript tabs
To create a tabs layout using file-based routing, use the following directory structure: src/app/_layout.tsx (root layout), src/app/(tabs)/_layout.tsx (tabs layout file), src/app/(tabs)/index.tsx (first tab), src/app/(tabs)/settings.tsx (additional tabs). The (tabs) directory is a special directory name that tells Expo Router to use the Tabs layout.
Root layout configuration for tabs
In the root src/app/_layout.tsx file, import Stack from 'expo-router' and render a Stack component with a Stack.Screen for the (tabs) group with the option headerShown set to false to hide the root header.
Tabs layout file structure
The (tabs)/_layout.tsx file is the main layout file for the tab bar and each tab. It defines how the tab bar and each tab button look and behave. Import Tabs from 'expo-router' and render a Tabs component with Tabs.Screen children for each tab. screenOptions prop applies options to all tabs, while individual Tabs.Screen options props apply to specific tabs.
Tabs.Screen options for tab configuration
Each Tabs.Screen component accepts a name prop (matching the file name) and an options prop. Common options include: title (the label shown in the tab bar), tabBarIcon (a function receiving {color} parameter that returns an icon component for that tab), and href (to customize the link behavior or hide the tab).
Tab file as default tab
The tab file named index.tsx is the default tab that loads when the app starts. Additional tabs are created by adding other files to the (tabs) directory.
JavaScript tabs extends React Navigation Bottom Tabs Navigator
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. You can use the same configuration props to customize the bottom tab bar and individual tabs after checking version compatibility.
Hiding a tab from the tab bar
To make a route exist but not show up in the tab bar, pass href: null in the Tabs.Screen options. This disables the tab button while keeping the route accessible.
Dynamic routes in tab bar
You can use a dynamic route in a tab bar. For example, a [user] tab that shows a user's profile. Use the href option to link to a specific user's profile. You can pass a string href like '/evanbacon' or an href object with pathname and params properties. When adding a dynamic route in your tab layout, the dynamic route must be unique—you cannot have two screens for the same dynamic route. If you need multiple dynamic routes, create a custom navigator.
Dynamic route href options
When configuring a dynamic route in a tab's options, you can use href in two ways: 1) href: '/evanbacon' (string form), or 2) href: { pathname: '/[user]', params: { user: 'evanbacon' } } (object form with pathname and params).
JavaScript tabs example code
Example of a complete JavaScript tabs layout:
```tsx
// src/app/_layout.tsx
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
);
}
// src/app/(tabs)/_layout.tsx
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>
);
}
// src/app/(tabs)/index.tsx & 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',
},
});
```
JavaScript tabs layout in Expo Router
Expo Router provides a JavaScript tabs layout implemented with React Navigation's bottom tabs navigator. It offers a familiar API to those who have already used React Navigation. JavaScript tabs is one of three tab navigator options available in Expo Router, alongside native tabs and custom tabs.
JavaScript Tabs component in layout
You can implement a JavaScript-based tab navigator using the Tabs component from 'expo-router'. All routes directly inside that directory become tabs. Use Tabs.Screen components to define each tab with a name prop matching the route file. You can set options like title and tabBarIcon. The index route becomes the default selected tab. The Tabs component uses React Navigation's native bottom tabs navigator.
JavaScript Tabs layout example
Example of a tabs layout: import { Tabs } from 'expo-router'; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; export default function TabLayout() { return ( <Tabs> <Tabs.Screen name="index" options={{ title: 'Home', tabBarIcon: ({ color }) => <MaterialIcons size={28} name="house.fill" color={color} />, }} /> <Tabs.Screen name="feed" options={{ title: 'Feed' }} /> <Tabs.Screen name="profile" options={{ title: 'Profile' }} /> </Tabs> ); }
Custom tabs using expo-router/ui components
On web, use custom tabs from 'expo-router/ui' with components: Tabs (root), TabSlot (placeholder for current route), TabList (container for tab triggers), and TabTrigger (individual tab). These are unstyled and flexible components. Example: <Tabs><TabSlot /><TabList><TabTrigger name="index" href="/">Home</TabTrigger><TabTrigger name="explore" href="/explore">Explore</TabTrigger></TabList></Tabs>