TabView
A container for primary tabs, each with its own navigation stack.
import { TabView } from '@satinhq/kit'Example
Declare the tabs
Register tab ids and root routes in defineNavigation. Use its defaultTab option for the initial tab. TabView renders those declarations, each with its own retained stack.
Control selection
Omit selection to let the TabView manage it. Supply selection and onSelectionChange for a controlled tab.
Adapt to larger windows
tabViewStyle="sidebarAdaptable" enables the supported sidebar adaptation. The compact preview shows the phone arrangement.
Add a bottom accessory
Pass persistent content, such as a mini player, to bottomAccessory. TabView places it relative to the tab bar, reserves its space in tab content, and owns safe-area clearance and movement with the tab chrome. It also adjusts placement for the supported iPad sidebar layout. Keep the accessory in this slot; do not position it over each tab or add another bottom inset to the tab screens.
On iPhone, the expanded accessory occupies a fixed 48-point row. Taller app content does not enlarge that row. On iPad, the slot has a 48-point minimum and can grow for taller content. Keep the phone composition compact.
TabView supplies regular capsule glass by default. Your app supplies the content, internal spacing and actions. When the app draws its own material, set bottomAccessoryMaterial="custom" to avoid a second glass wrapper.
Return <PlayerTabs /> from App in the tabs example above. The button toggles local state here; connect it to your media player's playback action in your app.
import { useState } from 'react'
import { Button, HStack, TabView, Text, VStack } from '@satinhq/kit'
export function PlayerTabs() {
const [playing, setPlaying] = useState(false)
return (
<TabView
bottomAccessory={
<HStack
spacing={12}
padding={{ horizontal: 12, vertical: 2 }}
frame={{ maxWidth: 'infinity', minHeight: 48 }}
>
<VStack alignment="leading" spacing={0} style={{ flex: 1, minWidth: 0 }}>
<Text font="headline" lineLimit={1}>Evening walk</Text>
<Text font="caption" foregroundStyle="secondary" lineLimit={1}>
Harbor sessions
</Text>
</VStack>
<Button
symbol={playing ? 'pause.fill' : 'play.fill'}
iconOnly
aria-label={playing ? 'Pause' : 'Play'}
buttonStyle="plain"
onPress={() => setPlaying(current => !current)}
/>
</HStack>
}
/>
)
}The full-width row and its content insets belong to this recipe. Text colors follow appearance. Accessory motion uses the framework's existing behavior; there is no public accessory-morph setting to configure.
API
Scroll horizontally to read every column.
| Prop | Type | Description |
|---|---|---|
selection | TabId | Optional controlled active tab. |
onSelectionChange | (id: TabId) => void | Receives a tab change. |
tabViewStyle | 'automatic' | 'sidebarAdaptable' | The adaptive style. |
bottomAccessoryMaterial | 'glass' | 'custom' | Defaults to glass. Use custom when the app supplies its own material. |
bottomAccessory | ReactNode | Persistent accessory content. TabView owns placement, clearance and default capsule glass; the app supplies content and controls. |
Related: Quick start, List, Screen.
Bottom accessory material
bottomAccessory receives regular capsule glass by default. Pass ordinary content:
<TabView bottomAccessory={<HStack><Text>Now Playing</Text></HStack>}>
{/* tabs */}
</TabView>Existing accessories that contain their own GlassEffect, including a custom component that renders glass, must set bottomAccessoryMaterial="custom". This leaves material ownership with the caller and avoids nested glass. The option changes material ownership only; placement and minimization remain TabView-owned.
For an existing mini-player component, migration is one prop:
<TabView bottomAccessoryMaterial="custom" bottomAccessory={<MiniPlayer />} />:::