SwiftUI TabView, Tab, TabSection, tab slots, bottom accessory, and Pager under One.iOS

One.iOS tab navigation and swipe paging are SwiftUI’s own. TabView is a
controlled tab view of Tab pages, with TabSection groups for the iPad
sidebar, TabViewSlot and TabViewBottomAccessory content around the bar, and
an optional Toolbar. Pager is a tab view without the bar: people swipe
between Page children.
Reach for TabView for top-level navigation with a bar, and Pager for swipe
flows with no chrome. selection must name a mounted page tab, or both
throw. Every view here throws on web and Android.
import { useState } from 'react'import { Text } from 'react-native'import { One } from 'one'
function FeedScreen() { return <Text>Your feed.</Text>}
function Library({ openComposer }: { openComposer: () => void }) { const [tab, setTab] = useState('feed')
return ( <One.iOS.TabView selection={tab} onSelectionChange={setTab}> <One.iOS.Tab value="feed" titleKey="Feed" systemImage="house"> <FeedScreen /> </One.iOS.Tab> <One.iOS.Tab value="new" titleKey="New" systemImage="plus" onPress={openComposer} /> </One.iOS.TabView> )}The first tab shows a page; the second is an action tab that runs its press
and never becomes the selection. onReselect reports a tap on the selected
tab.
Selection is a controlled tab value with an event count and a revision. A
native tab change bumps the count and reports the new id; JavaScript calls the
change handler and acknowledges the event. Raising revision with a new
selection moves the tab view from the outside. Action tabs and section header
buttons run their callbacks directly instead of changing the selection.
On iOS 18 and later each page renders in the modern Tab API inside its own
navigation container, which stays transparent so the system bar shows the
content behind it; your page owns its background. Older iOS falls back to a
legacy TabView. Sections, tab modifiers, styles, and customization need iOS
18. Tab customization edits persist as JSON: pass onCustomizationChange to
bind it, persist the reported string, and pass it back as customization.
TabView is a SwiftUI tab view. Only Tab, TabSection, TabViewSlot,
TabViewBottomAccessory, and one Toolbar may be its direct children.
import { useState } from 'react'import { Text } from 'react-native'import { One } from 'one'
function Home({ openComposer }: { openComposer: () => void }) { const [tab, setTab] = useState('feed')
return ( <One.iOS.TabView selection={tab} onSelectionChange={setTab} onReselect={(value) => console.log('reselected', value)} > <One.iOS.Tab value="feed" titleKey="Feed" systemImage="house"> <Text>Your feed.</Text> </One.iOS.Tab> <One.iOS.Tab value="new" titleKey="New" systemImage="plus" onPress={openComposer} /> </One.iOS.TabView> )}| Props | Type | Description |
|---|---|---|
selection (required)stringThe value of the selected page tab. | string | The value of the selected page tab. |
onSelectionChange (required)(value: string) => voidA native tab change, with the new value. | (value: string) => void | A native tab change, with the new value. |
onReselect(value: string) => voidA tap on the selected tab outside a TabSection, with its value. | (value: string) => void | A tap on the selected tab outside a TabSection, with its value. |
revisionnumber = 0Increase with a new selection to reset it from outside. | number = 0 | Increase with a new selection to reset it from outside. |
tabViewStyleTabViewStyle = "automatic"automatic, sidebarAdaptable, tabBarOnly, or page. | TabViewStyle = "automatic" | automatic, sidebarAdaptable, tabBarOnly, or page. |
tabBarVisibilityVisibility = "automatic"The tab bar on every page: automatic, visible, or hidden. | Visibility = "automatic" | The tab bar on every page: automatic, visible, or hidden. |
customizationstring = ""Saved tab customization JSON, from onCustomizationChange. | string = "" | Saved tab customization JSON, from onCustomizationChange. |
onCustomizationChange(customization: string) => voidPeople reordered or hid tabs. Persist the JSON. | (customization: string) => void | People reordered or hid tabs. Persist the JSON. |
swiftStyleOneNativeStyleSwiftUI modifiers on the tab view. | OneNativeStyle | SwiftUI modifiers on the tab view. |
children (required)ReactNodeTab, TabSection, TabViewSlot, and TabViewBottomAccessory elements. | ReactNode | Tab, TabSection, TabViewSlot, and TabViewBottomAccessory elements. |
Tab is one tab. It takes either children, as a page, or onPress, as an
action tab, never both. Values must be unique and nonempty, and systemImage
and image are mutually exclusive.
import { Text } from 'react-native'import { One } from 'one'
function SearchTab() { return ( <One.iOS.Tab value="search" titleKey="Search" systemImage="star" role="search" badge={3}> <Text>Search results.</Text> </One.iOS.Tab> )}| Props | Type | Description |
|---|---|---|
value (required)stringIdentifies the tab in selection. | string | Identifies the tab in selection. |
titleKey (required)stringThe tab label. | string | The tab label. |
systemImageSFSymbolName | ""An SF Symbol. Not with image. | SFSymbolName | "" | An SF Symbol. Not with image. |
imagestringAn asset catalog image. Not with systemImage. | string | An asset catalog image. Not with systemImage. |
labelStyleSDKLabelStyleiconOnly keeps the title for VoiceOver only. | SDKLabelStyle | iconOnly keeps the title for VoiceOver only. |
navigationStackbooleanSet false to skip the NavigationStack wrapper, so a nested NavigationSplitView renders. | boolean | Set false to skip the NavigationStack wrapper, so a nested NavigationSplitView renders. |
badgestring | numberA badge on the tab item. | string | number | A badge on the tab item. |
roleTabRolesearch or prominent, the SwiftUI tab roles. | TabRole | search or prominent, the SwiftUI tab roles. |
tabPlacementTabPlacementautomatic, pinned, or sidebarOnly. | TabPlacement | automatic, pinned, or sidebarOnly. |
testIDstringIdentifies the page in tests. | string | Identifies the page in tests. |
onPress() => voidMakes an action tab, which runs this and keeps the selection. | () => void | Makes an action tab, which runs this and keeps the selection. |
swiftStyleOneNativeStyleSwiftUI modifiers on the page content. | OneNativeStyle | SwiftUI modifiers on the page content. |
childrenReactNodeThe page. | ReactNode | The page. |
Tab and TabSection share these SwiftUI tab modifiers:
| Modifiers | Type | Description |
|---|---|---|
disabledbooleanDims the tab. iOS 18.4 or later. | boolean | Dims the tab. iOS 18.4 or later. |
hiddenbooleanRemoves the tab from the bar and sidebar. | boolean | Removes the tab from the bar and sidebar. |
customizationIDstringLets people reorder or hide the tab. | string | Lets people reorder or hide the tab. |
customizationBehavior{ behavior: TabCustomizationBehavior for?: readonly AdaptableTabBarPlacement[] }automatic, reorderable, or disabled, for the listed placements. | { behavior: TabCustomizationBehavior for?: readonly AdaptableTabBarPlacement[] } | automatic, reorderable, or disabled, for the listed placements. |
defaultVisibility{ visibility: Visibility for?: readonly AdaptableTabBarPlacement[] }Shown or hidden until people customize, for the listed placements. | { visibility: Visibility for?: readonly AdaptableTabBarPlacement[] } | Shown or hidden until people customize, for the listed placements. |
springLoadingBehaviorSpringLoadingBehaviorWhether a drag hovering the tab opens it. | SpringLoadingBehavior | Whether a drag hovering the tab opens it. |
accessibilityLabelstringRead by VoiceOver in place of the title. | string | Read by VoiceOver in place of the title. |
accessibilityHintstringWhat selecting the tab does. | string | What selecting the tab does. |
accessibilityValuestringThe tab's current value. | string | The tab's current value. |
accessibilityIdentifierstringIdentifies the tab item in UI tests. | string | Identifies the tab item in UI tests. |
helpstringHelp text for the tab. iOS 27 or later. | string | Help text for the tab. iOS 27 or later. |
TabSection groups tabs under a heading in the sidebar. Its children are
Tab elements, at least one.
import { useState } from 'react'import { Text } from 'react-native'import { One } from 'one'
function LibraryTabView() { const [tab, setTab] = useState('songs')
return ( <One.iOS.TabView tabViewStyle="sidebarAdaptable" selection={tab} onSelectionChange={setTab}> <One.iOS.TabSection id="music" title="Music"> <One.iOS.Tab value="songs" titleKey="Songs" systemImage="star"> <Text>Your songs.</Text> </One.iOS.Tab> </One.iOS.TabSection> </One.iOS.TabView> )}| Props | Type | Description |
|---|---|---|
id (required)stringIdentifies the section. | string | Identifies the section. |
title (required)stringThe section heading. | string | The section heading. |
defaultSectionExpansionTabSectionExpansionautomatic, expanded, or collapsed at first. | TabSectionExpansion | automatic, expanded, or collapsed at first. |
sectionActionsreadonly { id: string title: string systemImage?: SFSymbolName | "" onPress: () => void }[]Buttons in the section header. | readonly { id: string title: string systemImage?: SFSymbolName | "" onPress: () => void }[] | Buttons in the section header. |
children (required)ReactNodeThe tabs. | ReactNode | The tabs. |
TabViewSlot fills a named tab view slot with React Native content. Place it
directly inside TabView, alongside the Tab children. Each name may appear
once, the height must be above zero, and the slot keeps its declared height
and React Native event handlers as selection changes.
import { useState } from 'react'import { Pressable, Text } from 'react-native'import { One } from 'one'
function PlayerTabView({ openPlayer }: { openPlayer: () => void }) { const [tab, setTab] = useState('home')
return ( <One.iOS.TabView selection={tab} onSelectionChange={setTab}> <One.iOS.Tab value="home" titleKey="Home" systemImage="house"> <Text>Home.</Text> </One.iOS.Tab> <One.iOS.Tab value="other" titleKey="Other" systemImage="star"> <Text>Other.</Text> </One.iOS.Tab> <One.iOS.TabViewSlot name="tabViewBottomAccessory" height={50}> <Pressable onPress={openPlayer}> <Text>Open player</Text> </Pressable> </One.iOS.TabViewSlot> </One.iOS.TabView> )}The bottom accessory remains above the system tab bar as selection changes.
The sidebar slots are tabViewSidebarHeader, tabViewSidebarFooter, and
tabViewSidebarBottomBar: they display only when the tab view presents a
sidebar and are absent from the compact iPhone tab bar. On iPad, use
tabViewStyle="sidebarAdaptable" to present the native sidebar; the header
and footer sit above the sidebar bottom bar, which spans the bottom of the
sidebar.
import { useState } from 'react'import { Pressable, Text } from 'react-native'import { One } from 'one'
function SidebarTabView({ openSearch, openSettings, openProfile }: { openSearch: () => void; openSettings: () => void; openProfile: () => void }) { const [tab, setTab] = useState('home')
return ( <One.iOS.TabView tabViewStyle="sidebarAdaptable" selection={tab} onSelectionChange={setTab}> <One.iOS.Tab value="home" titleKey="Home" systemImage="house"> <Text>Home.</Text> </One.iOS.Tab> <One.iOS.Tab value="other" titleKey="Other" systemImage="star"> <Text>Other.</Text> </One.iOS.Tab> <One.iOS.TabViewSlot name="tabViewSidebarHeader" height={44}> <Pressable onPress={openSearch}> <Text>Search</Text> </Pressable> </One.iOS.TabViewSlot> <One.iOS.TabViewSlot name="tabViewSidebarFooter" height={48}> <Pressable onPress={openSettings}> <Text>Settings</Text> </Pressable> </One.iOS.TabViewSlot> <One.iOS.TabViewSlot name="tabViewSidebarBottomBar" height={52}> <Pressable onPress={openProfile}> <Text>Profile</Text> </Pressable> </One.iOS.TabViewSlot> </One.iOS.TabView> )}| Props | Type | Description |
|---|---|---|
name (required)TabViewSlotNametabViewBottomAccessory (iOS 26), or a sidebar slot (iOS 18). | TabViewSlotName | tabViewBottomAccessory (iOS 26), or a sidebar slot (iOS 18). |
height (required)numberThe slot height in points. Must be above zero. | number | The slot height in points. Must be above zero. |
children (required)ReactNodeThe slot content. | ReactNode | The slot content. |
TabViewBottomAccessory sits above the tab bar, like a music player’s now
playing bar. It needs iOS 26, and TabView takes one. It needs children,
inline, or expanded; a missing layout falls back to the others.
import { useState } from 'react'import { Text } from 'react-native'import { One } from 'one'
function PlayerBar() { const [tab, setTab] = useState('home')
return ( <One.iOS.TabView selection={tab} onSelectionChange={setTab}> <One.iOS.Tab value="home" titleKey="Home" systemImage="house"> <Text>Home.</Text> </One.iOS.Tab> <One.iOS.TabViewBottomAccessory inline={<Text>Evening Tide</Text>} expanded={<Text>Evening Tide, The Harbor Lights</Text>} /> </One.iOS.TabView> )}| Props | Type | Description |
|---|---|---|
isEnabledbooleanHides the accessory without remounting it. iOS 26.1 or later. | boolean | Hides the accessory without remounting it. iOS 26.1 or later. |
childrenReactNodeContent for both layouts. | ReactNode | Content for both layouts. |
inlineReactNodeContent while the bar is minimized beside it. | ReactNode | Content while the bar is minimized beside it. |
expandedReactNodeContent above the full bar. | ReactNode | Content above the full bar. |
Pager is a tab view without the bar: people swipe between pages. Children
must be Page elements with unique nonempty ids, and selection must name a
mounted page. For a pager on both platforms, use
One.UI.Pager.
Use indexDisplayMode="always" to show page indicators, "never" to hide them,
or "automatic" to let SwiftUI choose. The default is "automatic".
import { useState } from 'react'import { Text } from 'react-native'import { One } from 'one'
function Onboarding() { const [page, setPage] = useState('one')
return ( <One.iOS.Pager selection={page} onSelectionChange={setPage}> <One.iOS.Page id="one"> <Text>Welcome.</Text> </One.iOS.Page> <One.iOS.Page id="two"> <Text>How it works.</Text> </One.iOS.Page> </One.iOS.Pager> )}| Pager | Type | Description |
|---|---|---|
selection (required)stringThe id of the shown page. | string | The id of the shown page. |
indexDisplayModeSDKPageTabViewStyleIndexDisplayModeValue = 'automatic'When SwiftUI shows page indicators: automatic, always, or never. | SDKPageTabViewStyleIndexDisplayModeValue = 'automatic' | When SwiftUI shows page indicators: automatic, always, or never. |
onSelectionChange (required)(id: string) => voidA swipe to another page. | (id: string) => void | A swipe to another page. |
revisionnumber = 0Increase with a new selection to reset it from outside. | number = 0 | Increase with a new selection to reset it from outside. |
children (required)ReactNodePage elements. | ReactNode | Page elements. |
| Page | Type | Description |
|---|---|---|
id (required)stringIdentifies the page in selection. | string | Identifies the page in selection. |
testIDstringIdentifies the page in tests. | string | Identifies the page in tests. |
children (required)ReactNodeThe page content. | ReactNode | The page content. |
| iOS | Android | Web | |
|---|---|---|---|
| Renders | SwiftUI tabs | Throws | Throws |
| Sidebar | iPad sidebar | None | None |
| Paging | Swipe pages | None | None |
In the Peach simulator TabView draws the floating tab bar and honours
swiftStyle={{ tabBarMinimizeBehavior }}: the bar collapses as the page
scrolls down, and a tap on it expands it again. The page tab view style and
the iPad sidebar are not drawn there, so Peach throws for them.
tabBarMinimizeBehavior follows the page’s first scroll view. One names a
page’s first React Native ScrollView (or list) as the tab bar’s content
scroll view, so the bar minimizes as it scrolls with no extra wiring.TabView and Pager throw
otherwise, and an action tab never becomes the selection.Tab, TabSection,
TabViewSlot, TabViewBottomAccessory, and one Toolbar as direct
children throws; markers rendered anywhere else throw too.onPress,
and exactly one of systemImage and image.isEnabled iOS 26.1, and tab help text iOS 27.Edit this page on GitHub.