Skip to main content
StackNavigator makes your app’s mobile surface feel like a native app rather than a web page: a stack of screens that slide in, sheets that come up from the bottom, a tab bar with a stack per tab, large titles that fold into the bar, and the back gestures each platform expects. It draws the iOS idiom on iOS and the Material idiom on Android, and turns into a macOS or Windows window layout on desktop. The navigator takes its own full-screen, fixed-position layer. Use it as the root of your app’s mobile (or desktop) surface, not inside a page. Use one navigator per page.

The stack lives in history

Every push is a browser history entry. Android’s back button and gesture, the iOS edge swipe, the browser’s back button and your own Back button all arrive as the same history pop. You never write pushState or popstate handlers yourself, and back behaves correctly everywhere for free.

useStackNav

Route is your own type describing a screen. Keep routes to plain data with ids rather than objects; they are stored for restore and must survive JSON.
(route) => 'push' | 'modal' | 'sheet' | 'fullScreen'
How each route appears. Defaults to push for every route. See Presentations.
Tab[]
Tabs, each with its own stack. The first is the start tab. See Tabs.
(route) => ('medium' | 'large')[]
The heights a sheet route rests at, the first being where it opens. Default ['medium', 'large'].
string
Keep the stack in sessionStorage under this key, so a page reload rebuilds it without animation.
boolean | { minWidth?: number }
Two columns on wide screens. true means from 768 px wide. See Two columns.
({ route, tab, depth }) => void
The screen on top changed (pushed, popped, tab switched), and once on mount. route is null at a root. Use it for screen-view analytics or the document title.
It returns a nav object: Combine open with the SDK’s deep link API:

Presentations

A sheet is dragged by its grabber, its bar or content scrolled to the top: up to grow, down to half height, down from half to close. Tapping the dimmed area above closes it. It goes to full height when the keyboard opens. Inside a sheet, useStackSheet() returns { detent, detents, setDetent }. In two-column mode and on desktop, sheets and modals become centered cards.

Tabs

With tabs, every tab keeps its own stack, as a native tab bar controller does. Switching tabs leaves the other tab’s pushed screens, scroll positions and half-filled forms in place, hidden, and brings them back on return. Each tab’s root mounts the first time it is shown and then stays mounted. Draw the roots with root={(tab) => …} and put a StackTabBar in tabBar. Inside a navigator the tab bar binds itself: a tap switches tabs; a second tap on the current tab pops to its root, and at the root scrolls to the top. Android’s back walks the current tab’s stack. At the root of a tab other than the first, it goes to the first tab, as Material apps do. hidesTabBar={(route) => …} slides the tab bar away when a full-bleed screen (a reader, a player) is pushed, and back when it is popped. Don’t pass tabBar to that route’s StackScreen.

Two columns

With split, wide screens (tablets, desktop windows) show the root on the left and the first pushed screen on the right. Select from the root list with nav.show(route), which replaces the right column instead of stacking history, and mark the chosen row with GroupedRow selected. placeholder is what the right column shows before anything is picked, and primaryWidth (default 360) sets the left column’s width. On Android in two columns, the bottom tab bar becomes Material’s navigation rail on the left edge.

Guarding edited forms

useStackGuard asks before an edited screen is left, however it’s being closed: a Cancel button, Android’s back, or the iOS edge swipe. The stack puts the screen back and only lets the pop through on “discard”.
Outside a StackNavigator it does nothing.

Gestures and back

The navigator turns off the shell’s own WebView swipe while it runs, so the two don’t fight. In shells that predate this, the WebView’s gesture still works. ↑ and ↓ (and Home / End) move between rows on a screen, and Enter opens one. When a screen slides in, focus moves to its title so screen readers announce it; when you come back, focus returns to the row that opened it.
StackNav
required
The object from useStackNav.
ReactNode | (tab) => ReactNode
required
The root screen, or a function drawing each tab’s root.
(route) => ReactNode
required
Draws a pushed route, usually as a StackScreen.
ReactNode
A StackTabBar. It stays put while screens push under it and is covered by sheets.
(route) => boolean
Routes that slide the tab bar away.
'classic' | 'modern'
modern is iOS 26 Liquid Glass and Material 3 Expressive. Defaults to UIProvider’s look.
'ios' | 'android' | 'macos' | 'windows'
Force an idiom instead of detecting it.
boolean
default:"true"
Run the shell’s chrome to match: hide its header (every screen draws its own bar), color the status bar for the color scheme, and on Android tint the navigation bar like the tab bar. In the desktop shell the bar carries the window controls. Pass false to manage them yourself.
number
default:"360"
Left column width in two-column mode.
ReactNode
Right column content before anything is picked.
{ expandSheet?, collapseSheet? }
Screen-reader names of a sheet’s grabber. English by default.

Idioms and looks

useStackOS() returns the idiom in use: 'ios', 'android', 'macos' or 'windows'. isPhoneOS(os) tells the phone idioms apart. Use them where your own UI should differ, for example an “add” button in the bar on iOS and desktop but a fab on Android. On desktop the navigator draws a macOS window (a unified toolbar, a translucent source-list sidebar in two columns, sheets that drop from the top of the window) or a Windows 11 one (a Fluent bar, a NavigationView sidebar, centered dialogs). Phone-only parts such as the FAB and edge swipes don’t appear there. useStackLook() returns 'classic' or 'modern'. useStackTransitionEnd() is false until the screen has finished sliding in; <StackDeferred fallback={…}> renders heavy content only after that, so the slide stays smooth. useStackPaneRoot() is true for the first screen of the right column in two-column mode, which has no Back button.

Next

Screens and bars

StackScreen, bar buttons, search, tab bar

Grouped forms

Settings-style lists and form fields

Lists and pages

Swipe rows, long lists, paging

Keyboard shortcuts

useShortcut and shortcut labels