This component should only be rendered inside a _layout.tsx file, where it will serve as the location that children will render for routes below the layout.
Stack is simply a React Navigation Native Stack view and accepts the same props as React Navigation.
import { Stack } from 'one'import { Button } from 'react-native'
export default function Layout() { return ( <Stack screenOptions={{ headerRight() { return ( <Button label="Settings" /> ) }, }} /> )}You can customize the children of the Stack in your layout by passing a children prop to Stack has Stack.Screen elements, like so:
import { Stack } from 'one'
export default function Layout() { return ( <Stack> <Stack.Screen name="index" options={{ title: 'Feed' }} /> <Stack.Screen name="[id]" options={{ title: 'Post' }} /> <Stack.Screen name="sheet" options={{ presentation: 'formSheet', gestureDirection: 'vertical', animation: 'slide_from_bottom', headerShown: false, }} /> </Stack> )}The name must match the full name of the file inside app, without the extension but including groups.
In this example we are setting index, [id], and sheet screens, which would correspond to index.tsx and [id].tsx and sheet.tsx pages in the same directory.
This is a convenient way to configure settings for each page up front, but you could also render Stack.Screen inside each individual page so you can access data loaded inside that page. The upside of doing it in the layout is that it will configure things before any stack animation runs on enter, with the downside being that you can’t access page-level data.
The options property passes to the React Navigation NativeStack, and so takes the same options.
StackToolbar adds native iOS toolbar items to either side of the navigation header or to the bottom of a page. It uses UIBarButtonItem and UIToolbar, including SF Symbols, native menus, badges, and Liquid Glass appearance on iOS 26. It lives in the optional @vxrn/native package so apps that do not use it keep the native module out of their dependency graph and Metro bundle.
Install the native package directly in your app so React Native autolinking includes it:
npm install @vxrn/nativeimport { StackToolbar } from '@vxrn/native'
export default function Inbox() { return ( <> <StackToolbar placement="right"> <StackToolbar.Button icon="square.and.arrow.up" onPress={share}> Share </StackToolbar.Button> <StackToolbar.Menu icon="ellipsis.circle" title="Actions"> <StackToolbar.MenuAction icon="archivebox" onPress={archive}> Archive </StackToolbar.MenuAction> <StackToolbar.MenuAction icon="trash" destructive onPress={remove}> Delete </StackToolbar.MenuAction> </StackToolbar.Menu> </StackToolbar>
<StackToolbar> <StackToolbar.Button icon="photo.on.rectangle" onPress={selectPhotos}> Select </StackToolbar.Button> <StackToolbar.Spacer /> <StackToolbar.Button icon="plus" onPress={addPhoto}> Add </StackToolbar.Button> </StackToolbar>
<InboxContent /> </> )}The default placement is bottom. Render StackToolbar in a page component so it can configure that page’s native screen. Set placement="left" or placement="right" for header items:
<StackToolbar placement="left"> <StackToolbar.Button icon="sidebar.left" onPress={openSidebar} /></StackToolbar>Toolbar components:
StackToolbar.Button renders a native action. Use an SF Symbol name in icon, or compose Icon, Label, and Badge children.StackToolbar.Menu renders a native menu and supports nested or inline submenus.StackToolbar.MenuAction supports selected, disabled, destructive, hidden, and keep-presented states.StackToolbar.Spacer renders flexible bottom spacing when width is omitted, or fixed spacing when it is set.StackToolbar.View embeds a custom React Native view.StackToolbar.SearchBarSlot places a configured native search bar in the bottom toolbar on iOS 26 or newer.This API is alpha and currently renders on iOS. On web and Android it renders nothing. Use separateBackground, hidesSharedBackground, and badges to control the native iOS 26 grouping and appearance without recreating the toolbar in JavaScript.
For more declarative header configuration, use the compositional API with Stack.Header and its child components:
import { Stack } from 'one'
export default function Layout() { return ( <Stack> <Stack.Screen name="index"> <Stack.Header blurEffect="regular"> <Stack.Header.Title large>Articles</Stack.Header.Title> <Stack.Header.SearchBar placeholder="Search..." /> </Stack.Header> </Stack.Screen>
<Stack.Screen name="[id]"> <Stack.Header> <Stack.Header.Title>Post</Stack.Header.Title> <Stack.Header.Right asChild> <ShareButton /> </Stack.Header.Right> </Stack.Header> </Stack.Screen> </Stack> )}The container for header configuration. Props:
hidden - Hide the header entirelyblurEffect - iOS blur effect ('regular', 'prominent', 'systemMaterial', etc.)asChild - Render a completely custom header componentstyle - Style with backgroundColor, shadowColor (set to 'transparent' to hide)largeStyle - Style for large title modeConfigure the header title:
<Stack.Header.Title large>My Title</Stack.Header.Title>Props:
children - The title textlarge - Enable iOS large title modestyle - Text style (fontWeight, fontSize, color, textAlign)Configure the back button:
<Stack.Header.BackButton hidden /><Stack.Header.BackButton displayMode="minimal">Back</Stack.Header.BackButton>Props:
children - Custom back button texthidden - Hide the back buttondisplayMode - 'default', 'generic', or 'minimal'withMenu - Enable long-press menu on iOSAdd custom components to the header:
<Stack.Header.Left asChild> <MenuButton /></Stack.Header.Left>
<Stack.Header.Right asChild> <SettingsButton /></Stack.Header.Right>Props:
asChild - Required to render custom childrenchildren - Your custom componentAdd an iOS-style search bar:
<Stack.Header.SearchBar placeholder="Search articles..." autoCapitalize="none" placement="stacked"/>Props:
placeholder - Placeholder textautoCapitalize - 'none', 'words', 'sentences', 'characters'placement - 'automatic' or 'stacked'hideWhenScrolling - Hide when scrollingobscureBackground - Obscure background when activeYou can set a default header for all screens by placing Stack.Header directly inside Stack:
<Stack> <Stack.Header blurEffect="regular"> <Stack.Header.BackButton displayMode="minimal" /> </Stack.Header>
<Stack.Screen name="index" options={{ title: 'Home' }} /> <Stack.Screen name="profile" options={{ title: 'Profile' }} /></Stack>renderOn native, presentations like formSheet, pageSheet, and modal are handled by react-native-screens and render as real native sheets. On web there is no built-in equivalent - by default an overlay route shows as a card.
The render prop on <Stack> lets you plug in any sheet or modal library to render overlay routes on web. Tamagui Sheet, Vaul, Radix Dialog, or a hand-rolled element all work.
import { Stack, type StackRender } from 'one'import { Sheet } from 'tamagui'
// Hoist outside the layout to keep identity stable across re-renders.const render: StackRender = { web: ({ children, open, dismiss, dismissible, sheetAllowedDetents }) => ( <Sheet modal open={open} onOpenChange={(o) => !o && dismiss()} dismissOnSnapToBottom={dismissible} snapPoints={sheetAllowedDetents} > <Sheet.Overlay /> <Sheet.Frame>{children}</Sheet.Frame> </Sheet> ),}
export default function Layout() { return ( <Stack render={render}> <Stack.Screen name="filters" options={{ presentation: 'formSheet', sheetAllowedDetents: [0.5, 1], sheetGrabberVisible: true, }} /> </Stack> )}The render component receives the overlay route’s content as children along with normalized props:
open: boolean - whether the overlay should be visible. Toggles as you navigate.dismiss(): void - call after your close animation completes. Triggers StackActions.pop.dismissible: boolean - matches options.gestureEnabled.presentation - the route’s presentation option.routeKey, routeName - the route identity.sheetAllowedDetents, sheetGrabberVisible, sheetCornerRadius, sheetInitialDetentIndex, sheetLargestUndimmedDetentIndex, sheetExpandsWhenScrolledToEdge) pass through unchanged.Pass render inside options to override for a single route:
<Stack render={defaultRender}> <Stack.Screen name="filters" options={{ presentation: 'formSheet', render: { web: SpecialFilterChrome }, }} /></Stack>Resolution: per-route options.render → <Stack render> → setupRendering global → default (renders inline).
If every <Stack> in your app should use the same overlay render, register it once from your setup file:
import { setupRendering } from 'one'import { Sheet } from 'tamagui'
setupRendering({ Stack: { web: ({ children, open, dismiss, sheetAllowedDetents }) => ( <Sheet modal open={open} onOpenChange={(o) => !o && dismiss()} snapPoints={sheetAllowedDetents}> <Sheet.Overlay /> <Sheet.Frame>{children}</Sheet.Frame> </Sheet> ), },})Wire the setup file in vite.config.ts:
import { one } from 'one/vite'
export default { plugins: [ one({ setupFile: './app/setup.ts', }), ],}You can split per environment if needed: setupFile: { client: './client.ts', server: './server.ts', native: './native.ts' }.
By default, dismissing an overlay route pops it from the navigation state and React unmounts the route component. To preserve state (form values, useId, useState, refs) across close → reopen, set keepMounted: true on the screen options:
<Stack.Screen name="settings" options={{ presentation: 'formSheet', keepMounted: true, }}/>When keepMounted is on:
open: false.useId returns the same value, useState retains its value, refs are preserved.The render component must keep children in the React tree when open: false - typically by toggling visibility rather than conditionally rendering. Tamagui Sheet’s modal mode does this automatically. A render that returns null when closed will unmount the children regardless of keepMounted.
// ❌ unmounts on close - keepMounted has no effectconst render: StackRender = { web: ({ children, open }) => (open ? <div>{children}</div> : null),}
// ✅ keeps children mounted, just hiddenconst render: StackRender = { web: ({ children, open, dismiss }) => ( <Sheet modal open={open} onOpenChange={(o) => !o && dismiss()}> <Sheet.Frame>{children}</Sheet.Frame> </Sheet> ),}Caveats:
[id]-style dynamic routes.<Stack> itself.Edit this page on GitHub.