One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

iOS TabView and Pager

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

One.iOS TabView with a glass tab bar, a badge, a search tab, and a now playing accessory

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.

Usage

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.

How it works

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

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>
)
}
PropsTypeDescription
selection (required)stringThe value of the selected page tab.
stringThe value of the selected page tab.
onSelectionChange (required)(value: string) => voidA native tab change, with the new value.
(value: string) => voidA native tab change, with the new value.
onReselect(value: string) => voidA tap on the selected tab outside a TabSection, with its value.
(value: string) => voidA tap on the selected tab outside a TabSection, with its value.
revisionnumber = 0Increase with a new selection to reset it from outside.
number = 0Increase 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) => voidPeople reordered or hid tabs. Persist the JSON.
swiftStyleOneNativeStyleSwiftUI modifiers on the tab view.
OneNativeStyleSwiftUI modifiers on the tab view.
children (required)ReactNodeTab, TabSection, TabViewSlot, and TabViewBottomAccessory elements.
ReactNodeTab, TabSection, TabViewSlot, and TabViewBottomAccessory elements.

Tab

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>
)
}
PropsTypeDescription
value (required)stringIdentifies the tab in selection.
stringIdentifies the tab in selection.
titleKey (required)stringThe tab label.
stringThe tab label.
systemImageSFSymbolName | ""An SF Symbol. Not with image.
SFSymbolName | ""An SF Symbol. Not with image.
imagestringAn asset catalog image. Not with systemImage.
stringAn asset catalog image. Not with systemImage.
labelStyleSDKLabelStyleiconOnly keeps the title for VoiceOver only.
SDKLabelStyleiconOnly keeps the title for VoiceOver only.
navigationStackbooleanSet false to skip the NavigationStack wrapper, so a nested NavigationSplitView renders.
booleanSet false to skip the NavigationStack wrapper, so a nested NavigationSplitView renders.
badgestring | numberA badge on the tab item.
string | numberA badge on the tab item.
roleTabRolesearch or prominent, the SwiftUI tab roles.
TabRolesearch or prominent, the SwiftUI tab roles.
tabPlacementTabPlacementautomatic, pinned, or sidebarOnly.
TabPlacementautomatic, pinned, or sidebarOnly.
testIDstringIdentifies the page in tests.
stringIdentifies the page in tests.
onPress() => voidMakes an action tab, which runs this and keeps the selection.
() => voidMakes an action tab, which runs this and keeps the selection.
swiftStyleOneNativeStyleSwiftUI modifiers on the page content.
OneNativeStyleSwiftUI modifiers on the page content.
childrenReactNodeThe page.
ReactNodeThe page.

Tab and TabSection share these SwiftUI tab modifiers:

ModifiersTypeDescription
disabledbooleanDims the tab. iOS 18.4 or later.
booleanDims the tab. iOS 18.4 or later.
hiddenbooleanRemoves the tab from the bar and sidebar.
booleanRemoves the tab from the bar and sidebar.
customizationIDstringLets people reorder or hide the tab.
stringLets 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.
SpringLoadingBehaviorWhether a drag hovering the tab opens it.
accessibilityLabelstringRead by VoiceOver in place of the title.
stringRead by VoiceOver in place of the title.
accessibilityHintstringWhat selecting the tab does.
stringWhat selecting the tab does.
accessibilityValuestringThe tab's current value.
stringThe tab's current value.
accessibilityIdentifierstringIdentifies the tab item in UI tests.
stringIdentifies the tab item in UI tests.
helpstringHelp text for the tab. iOS 27 or later.
stringHelp text for the tab. iOS 27 or later.

TabSection

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>
)
}
PropsTypeDescription
id (required)stringIdentifies the section.
stringIdentifies the section.
title (required)stringThe section heading.
stringThe section heading.
defaultSectionExpansionTabSectionExpansionautomatic, expanded, or collapsed at first.
TabSectionExpansionautomatic, 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.
ReactNodeThe tabs.

TabViewSlot

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>
)
}
PropsTypeDescription
name (required)TabViewSlotNametabViewBottomAccessory (iOS 26), or a sidebar slot (iOS 18).
TabViewSlotNametabViewBottomAccessory (iOS 26), or a sidebar slot (iOS 18).
height (required)numberThe slot height in points. Must be above zero.
numberThe slot height in points. Must be above zero.
children (required)ReactNodeThe slot content.
ReactNodeThe slot content.

TabViewBottomAccessory

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>
)
}
PropsTypeDescription
isEnabledbooleanHides the accessory without remounting it. iOS 26.1 or later.
booleanHides the accessory without remounting it. iOS 26.1 or later.
childrenReactNodeContent for both layouts.
ReactNodeContent for both layouts.
inlineReactNodeContent while the bar is minimized beside it.
ReactNodeContent while the bar is minimized beside it.
expandedReactNodeContent above the full bar.
ReactNodeContent above the full bar.

Pager, Page

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>
)
}
PagerTypeDescription
selection (required)stringThe id of the shown page.
stringThe 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) => voidA swipe to another page.
revisionnumber = 0Increase with a new selection to reset it from outside.
number = 0Increase with a new selection to reset it from outside.
children (required)ReactNodePage elements.
ReactNodePage elements.
PageTypeDescription
id (required)stringIdentifies the page in selection.
stringIdentifies the page in selection.
testIDstringIdentifies the page in tests.
stringIdentifies the page in tests.
children (required)ReactNodeThe page content.
ReactNodeThe page content.

Platform differences

iOSAndroidWeb
RendersSwiftUI tabsThrowsThrows
SidebariPad sidebarNoneNone
PagingSwipe pagesNoneNone

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.

Limits and gotchas

Edit this page on GitHub.