One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

iOS Navigation and Toolbar

SwiftUI NavigationStack, NavigationSplitView, and Toolbar entries under One.iOS

One.iOS NavigationStack with an inline title and toolbar buttons drawn by SwiftUI

One.iOS navigation chrome is SwiftUI’s own: a real NavigationStack whose root is the React Native content beside it, with the navigation bar filled by Toolbar entries, and a two or three column NavigationSplitView for sidebar layouts. Title, display mode, and every other scalar navigation modifier reach the stack through swiftStyle, so the bar has no bespoke props.

This is the SwiftUI toolbar that fills the navigation bar, distinct from the UIKit bottom toolbar (One.iOS.ToolbarHost and One.iOS.BarButtonItem) covered in Toolbar and Menus.

Usage

import { Text } from 'react-native'
import { One } from 'one'
function Mailbox({ save }: { save: () => void }) {
return (
<One.iOS.NavigationStack
style={{ flex: 1 }}
swiftStyle={{ navigationTitleWithText: 'Mailbox', navigationBarTitleDisplayMode: 'inline' }}
>
<One.iOS.Toolbar>
<One.iOS.ToolbarItem placement="primaryAction">
<One.iOS.Button label="Save" onPress={save} />
</One.iOS.ToolbarItem>
</One.iOS.Toolbar>
<Text>Your messages.</Text>
</One.iOS.NavigationStack>
)
}

The toolbar fills the navigation bar while the React Native text stays the stack root. Set the title with swiftStyle, for example navigationTitleWithText: 'Mailbox' with navigationBarTitleDisplayMode: 'inline'.

How it works

The stack renders through a Fabric view hosting a SwiftUI NavigationStack. At render, children split into two groups: Toolbar elements, which read as ordered toolbar entries, and everything else, which becomes the root content hosted in the box SwiftUI proposes. The stack merges every toolbar it is given. A split view instead takes column markers, each hosting its own React Native content with an optional toolbar.

Toolbar entries are markers: Toolbar, ToolbarItem, ToolbarItemGroup, ToolbarSpacer, and the column markers never render where they are written and throw when placed anywhere but their parent. Placements use SwiftUI names checked against the device’s iOS version, so a placement newer than the running iOS throws before any native prop carries it.

NavigationStack is a SwiftUI navigation stack whose root is the React Native content beside it. Children are Toolbar elements or React Native content; ToolbarItem, ToolbarItemGroup, and ToolbarSpacer belong inside a Toolbar, never directly in the stack.

import { Text } from 'react-native'
import { One } from 'one'
function Inbox({ save }: { save: () => void }) {
return (
<One.iOS.NavigationStack style={{ flex: 1 }}>
<One.iOS.Toolbar>
<One.iOS.ToolbarItem placement="primaryAction">
<One.iOS.Button label="Save" onPress={save} />
</One.iOS.ToolbarItem>
</One.iOS.Toolbar>
<Text>Your messages.</Text>
</One.iOS.NavigationStack>
)
}
PropsTypeDescription
children (required)ReactNodeToolbar elements and the root content.
ReactNodeToolbar elements and the root content.

Bar modifiers

The stack reads these swiftStyle modifiers inside the NavigationStack, so each one changes the navigation bar of the stack it is set on.

<One.iOS.NavigationStack
style={{ flex: 1 }}
swiftStyle={{
navigationTitleWithText: 'Accounts',
navigationBarTitleDisplayMode: 'inline',
toolbarBackground: { visibility: 'visible', bars: 'navigationBar' },
toolbarColorScheme: { colorScheme: 'dark', bars: 'navigationBar' },
navigationBarBackButtonHidden: true,
}}
>
<Text>Your accounts.</Text>
</One.iOS.NavigationStack>
ModifierWhat it changes
navigationTitleWithText, navigationBarTitleDisplayModeThe bar title, shown large or inline.
toolbarBackground, toolbarBackgroundVisibilityWhether the bar draws its own background or lets content show through.
toolbarColorSchemeThe light or dark scheme the bar is drawn in.
toolbarVisibilityShows or hides the bar.
navigationBarBackButtonHiddenHides the back button on a pushed screen.
searchableAdds the search field and reports its text back.

The Peach simulator draws each of these for the navigation bar. A bars value other than automatic or navigationBar belongs to a tab bar or bottom toolbar, so Peach throws instead of guessing, as it does for any other navigation or toolbar modifier it does not yet draw.

NavigationSplitView is SwiftUI’s two or three column split view. Its direct children are one Sidebar, an optional Content, and one Detail. Each column accepts React Native content, one Toolbar whose entries sit inside Toolbar.Content, and a column swiftStyle for its navigation title and other column-level modifiers.

import { useState } from 'react'
import { Text } from 'react-native'
import { One } from 'one'
function Mail({ mailboxes, addMailbox }: { mailboxes: { id: string; name: string }[]; addMailbox: () => void }) {
const [selection, setSelection] = useState<string[]>([])
return (
<One.iOS.NavigationSplitView style={{ flex: 1 }}>
<One.iOS.NavigationSplitView.Sidebar
swiftStyle={{ navigationTitleWithText: 'Mailboxes' }}
>
<One.iOS.Toolbar>
<One.iOS.List selection={selection} onSelectionChange={setSelection}>
{mailboxes.map((mailbox) => (
<One.iOS.Label
key={mailbox.id}
label={mailbox.name}
systemImage="tray"
swiftStyle={{ tag: mailbox.id }}
/>
))}
</One.iOS.List>
<One.iOS.Toolbar.Content>
<One.iOS.ToolbarItem placement="topBarTrailing">
<One.iOS.Button label="Add" onPress={addMailbox} />
</One.iOS.ToolbarItem>
</One.iOS.Toolbar.Content>
</One.iOS.Toolbar>
</One.iOS.NavigationSplitView.Sidebar>
<One.iOS.NavigationSplitView.Detail>
<Text>Select a mailbox.</Text>
</One.iOS.NavigationSplitView.Detail>
</One.iOS.NavigationSplitView>
)
}

Omit columnVisibility and preferredCompactColumn to let SwiftUI manage them. Supplying either prop makes it controlled: its callback reports the native proposal, and the prop remains authoritative until React updates it. columnVisibility is automatic, all, doubleColumn, or detailOnly; preferredCompactColumn is sidebar, content, or detail and applies on iOS 17 or later.

PropsTypeDescription
columnVisibilityNavigationSplitViewVisibility = "automatic"The controlled visibility of the columns.
NavigationSplitViewVisibility = "automatic"The controlled visibility of the columns.
onColumnVisibilityChange(visibility: NavigationSplitViewVisibility) => voidReceives a native visibility proposal.
(visibility: NavigationSplitViewVisibility) => voidReceives a native visibility proposal.
preferredCompactColumnNavigationSplitViewColumn = "sidebar"The controlled compact column. Applies on iOS 17 or later.
NavigationSplitViewColumn = "sidebar"The controlled compact column. Applies on iOS 17 or later.
onPreferredCompactColumnChange(column: NavigationSplitViewColumn) => voidReceives a native compact-column proposal.
(column: NavigationSplitViewColumn) => voidReceives a native compact-column proposal.
children (required)ReactNodeA Sidebar, optional Content, and Detail column.
ReactNodeA Sidebar, optional Content, and Detail column.

Toolbar

Toolbar becomes one native toolbar. In a NavigationStack or TabView, its children are ToolbarItem, ToolbarItemGroup, or ToolbarSpacer elements in order. A split view column uses Toolbar as a content wrapper and puts those entries inside Toolbar.Content. NavigationStack merges every toolbar it is given, and One.iOS.TabView also takes a Toolbar child.

import { One } from 'one'
function SaveToolbar({ save }: { save: () => void }) {
return (
<One.iOS.Toolbar>
<One.iOS.ToolbarItem placement="primaryAction">
<One.iOS.Button label="Save" onPress={save} />
</One.iOS.ToolbarItem>
</One.iOS.Toolbar>
)
}
PropsTypeDescription
children (required)ReactNodeToolbar entries, or column content with a Toolbar.Content marker.
ReactNodeToolbar entries, or column content with a Toolbar.Content marker.

Toolbar.Content

Toolbar.Content marks the toolbar entries inside a split view column’s content wrapper. Its children follow the same order and types as a regular toolbar: one Toolbar.Content per toolbar, and it needs children.

ToolbarItem

ToolbarItem places one item in the bar. It needs children.

import { One } from 'one'
function EditItem({ edit }: { edit: () => void }) {
return (
<One.iOS.ToolbarItem placement="topBarTrailing">
<One.iOS.Button label="Edit" onPress={edit} />
</One.iOS.ToolbarItem>
)
}
PropsTypeDescription
placementToolbarItemPlacement = "automatic"Where the item goes. See below.
ToolbarItemPlacement = "automatic"Where the item goes. See below.
children (required)ReactNodeThe item, such as a One.iOS.Button.
ReactNodeThe item, such as a One.iOS.Button.

ToolbarItemPlacement is every SwiftUI placement: automatic, principal, navigation, primaryAction, secondaryAction, status, confirmationAction, cancellationAction, destructiveAction, keyboard, topBarLeading, topBarTrailing, topBarPinnedTrailing, navigationBarLeading, navigationBarTrailing, title, largeTitle, bottomBar, subtitle, and largeSubtitle. A placement newer than the running iOS throws.

ToolbarItemGroup

ToolbarItemGroup places several items together. It needs children, and a systemImage requires a label.

import { One } from 'one'
function PageControls({ back, forward }: { back: () => void; forward: () => void }) {
return (
<One.iOS.ToolbarItemGroup placement="bottomBar" label="Pages">
<One.iOS.Button label="Back" onPress={back} />
<One.iOS.Button label="Forward" onPress={forward} />
</One.iOS.ToolbarItemGroup>
)
}
PropsTypeDescription
placementToolbarItemPlacement = "automatic"Where the group goes.
ToolbarItemPlacement = "automatic"Where the group goes.
labelstring = ""Names the group when SwiftUI collapses it.
string = ""Names the group when SwiftUI collapses it.
systemImageSFSymbolName | "" = ""An SF Symbol with the label. Needs a label.
SFSymbolName | "" = ""An SF Symbol with the label. Needs a label.
children (required)ReactNodeThe items.
ReactNodeThe items.

DefaultToolbarItem

DefaultToolbarItem places one of the system’s own toolbar items, such as the search field, where you say. It needs iOS 26 and takes no children. With kind="search" and placement="bottomBar" on a list that is searchable, plus searchToolbarBehavior: 'minimize' in the list’s swiftStyle, the field becomes the detached search circle iPhone shows at the bottom of the screen.

import { One } from 'one'
function BottomSearchToolbar() {
return (
<One.iOS.Toolbar>
<One.iOS.DefaultToolbarItem kind="search" placement="bottomBar" />
<One.iOS.ToolbarSpacer sizing="flexible" placement="bottomBar" />
</One.iOS.Toolbar>
)
}
PropsTypeDescription
kind (required)ToolbarDefaultItemKindWhich system item to place, such as search.
ToolbarDefaultItemKindWhich system item to place, such as search.
placementToolbarItemPlacementWhere in the bars the item goes.
ToolbarItemPlacementWhere in the bars the item goes.

ToolbarSpacer

ToolbarSpacer spreads toolbar items apart. It needs iOS 26 and takes no children.

import { One } from 'one'
function SpacedToolbar({ save }: { save: () => void }) {
return (
<One.iOS.Toolbar>
<One.iOS.ToolbarItem placement="topBarLeading">
<One.iOS.Button label="Save" onPress={save} />
</One.iOS.ToolbarItem>
<One.iOS.ToolbarSpacer sizing="flexible" />
</One.iOS.Toolbar>
)
}
PropsTypeDescription
sizingSpacerSizing = "flexible"flexible fills the space; fixed adds a standard gap.
SpacerSizing = "flexible"flexible fills the space; fixed adds a standard gap.
placementToolbarItemPlacement = "automatic"The bar the spacer sits in.
ToolbarItemPlacement = "automatic"The bar the spacer sits in.

Platform differences

iOSAndroidWeb
RendersSwiftUI chromeThrowsThrows
RootNative contentNoneNone
ColumnsSplit columnsNoneNone

Limits and gotchas

Edit this page on GitHub.