This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.
Custom navigators
Edit page
Learn how to build your own navigator in Expo Router and how library authors can integrate an existing navigator with the router.
Expo Router ships with navigators for the most common patterns — Stack, Tabs, Native tabs, and Drawer. When none of them fit, you can build your own navigator and use it as a layout, with file-based routing, deep linking, and typed routes working exactly as they do for the built-in navigators.
Choose the entry point that matches your goal:
- App developers building a navigator for one app use
createStandardRouterNavigator. - Library authors shipping a reusable navigator for both Expo Router and React Navigation use
integrateWithRouter.
The stable
createStandardRouterNavigatorandintegrateWithRouterAPIs are available in SDK 58 and later. In SDK 56 and SDK 57, useunstable_createStandardRouterNavigatorandunstable_integrateWithRouterinstead.
For a stack navigator that renders routes as web modal overlays, see Build custom web modals.
Create a navigator in your app
Use createStandardRouterNavigator to turn a content component into a navigator you can render as a layout. It takes two required arguments:
NavigatorContent: a component that renders your navigator's UI. It receives the current navigationstate, thedescriptorsfor each screen,actionsto navigate, and anemitterto send events.router: the routing behavior to use. ImportStackRouterfor stack-like navigation orTabRouterfor tab-like navigation fromexpo-router.
The following example builds a minimal tab navigator:
The returned navigator has a .Screen child for declaring screens, so you can use it in a _layout file like any other layout:
What NavigatorContent receives
Typed events
If your navigator emits events, declare them in the second type argument to NavigatorContentProps. Each key is an event name, and its value describes the event's data and whether it canPreventDefault. emitter.emit is then typed against that map — unknown event names and mismatched payloads are rejected:
type TabsContentProps = NavigatorContentProps< { title?: string }, { tabPress: { data: undefined; canPreventDefault: true } } >; function TabsContent({ emitter }: TabsContentProps) { emitter.emit({ type: 'tabPress', canPreventDefault: true }); // ... }
createStandardRouterNavigator infers the event map from the component, so you do not pass it again at the call site. Omit the second type argument for a navigator that emits no events.
Options
Both createStandardRouterNavigator and integrateWithRouter accept an optional options object as their third argument. Use createProps to derive navigator-specific props that are not part of the standard state and actions:
export const Tabs = createStandardRouterNavigator(TabsContent, TabRouter, { createProps: ({ state, dispatch }) => ({ activeRouteKey: state.routes[state.index].key, preload: (name: string) => dispatch({ type: 'PRELOAD', payload: { name } }), }), });
Declare the props returned by createProps in the fourth type argument to NavigatorContentProps so NavigatorContent receives them in a typed way:
type TabsContentProps = NavigatorContentProps< { title?: string }, // No custom events in this example. Record<string, never>, // No custom navigator props in this example. object, // Props injected by `createProps`. { activeRouteKey: string; preload: (name: string) => void } >; function TabsContent({ activeRouteKey, preload }: TabsContentProps) { // ... }
createPropsreceives the processed Expo Routerstateand rawdispatch. These are internal and may have small breaking changes between releases, so prefer thestateandactionspassed toNavigatorContentwhen they suffice. If something you need is missing from the standardstate,actions, oremitter, open an issue on GitHub.
Integrate an existing navigator (library authors)
The standard navigator API
The NavigatorContent component shown above is a standard navigator. It implements a minimal, framework-agnostic contract defined by the standard-navigation package. The state, descriptors, actions, and emitter your content receives are exactly the same API as the in-app navigator above. The only difference is who creates the navigator.
createStandardRouterNavigator is a shortcut that calls createStandardNavigator (from standard-navigation) for you and integrates the result with Expo Router in one step. As a library author, call createStandardNavigator yourself and keep a reference to the navigator:
Because TabsContent and navigator depend only on the standard contract, the same code runs on Expo Router, React Navigation, or any other host that implements it. You write the navigator once and ship a thin integration entry point per framework.
Integrate with Expo Router
Wire your navigator into Expo Router with integrateWithRouter:
The returned component works exactly like the one from createStandardRouterNavigator, including the .Screen child and the same options.
Add props for a built-in navigator
The helpers in this section are available in SDK 58 and later.
When your library wraps an Expo Router navigator, pass its createProps helper to integrateWithRouter. The helper adds the navigator-specific behavior that Expo Router expects.
For example, integrate a JavaScript stack like this:
For a lower-level implementation, use createBaseStackProps or createBaseTabProps from expo-router and add the behavior your navigator needs.
Library entry points
Keep the navigator content and the standard navigator framework-agnostic, then expose one entry point per framework so consumers import the integration that matches their app:
.srcTabsContent.tsxNavigator UI implementing the standard navigator APIindex.tsRoot entry — exports the framework-agnostic navigatorreact-navigation.tsReact Navigation entry — integrates the same navigatorexpo-router.tsExpo Router entry — integrateWithRouter(navigator, ...)package.jsonMaps subpath exports to each framework entryMap each entry point to a subpath export in your library's package.json, pointing at your build output:
Consumers then import the integration for their framework (for example, import { Tabs } from 'my-tabs/expo-router') while you maintain the navigator logic in one place.
Learn how to integrate the same standard navigator with React Navigation, and read the contract that defines the state, descriptors, actions, and emitter your NavigatorContent receives.
Customize router behavior
extendRouterandextendRouterActionsare available in SDK 58 and later.
Use extendRouterActions when you only need to handle or reject navigation actions. Return a result to handle the action, null to reject it, or undefined to let the base router handle it.
Use extendRouter when you need to customize other router members, such as actionCreators, getStateForRouteFocus, or normalizeState. Members you do not return are inherited from the base router.
The following example adds a CLEAR action and an action creator for it:
Both helpers provide baseRouter, options, and nextKey. Use baseRouter to delegate existing behavior, options to read values passed to the router factory, and nextKey when adding a route to the state.