TabView
A SwiftUI TabView component for paged or tabbed content.
Expo UI TabView matches the official SwiftUI TabView API and switches between styles via the tabViewStyle modifier.
Note: For routed bottom-tab navigation across full-screen routes, use
expo-router/unstable-native-tabsinstead.
Usage
Each page is a <TabView.Tab> child, identified by a value prop. TabView does not impose its own height — give it a frame, or place it inside a parent that does.
Page style (swipeable pager)
Use tabViewStyle({ type: 'page' }) for a horizontal pager with optional dot indicators. Pass defaultSelection to start the pager on a specific page without controlling it from React.
import {
Host,
Spacer,
TabView,
Text,
VStack,
} from '@expo/ui/swift-ui';
import {
background,
font,
foregroundStyle,
frame,
tabViewStyle,
} from '@expo/ui/swift-ui/modifiers';
const fillFrame = frame({
maxWidth: Infinity,
maxHeight: Infinity,
});
const pageFrame = frame({ minHeight: 320, maxHeight: 320 });
export default function PagerExample() {
return (
<Host style={{ flex: 1 }}>
<TabView
defaultSelection="1"
modifiers={[pageFrame, tabViewStyle({ type: 'page' })]}>
<TabView.Tab value="0">
<Page label="Page 1" color="#6200EE" />
</TabView.Tab>
<TabView.Tab value="1">
<Page label="Page 2" color="#03DAC5" />
</TabView.Tab>
<TabView.Tab value="2">
<Page label="Page 3" color="#FF5722" />
</TabView.Tab>
</TabView>
</Host>
);
}
function Page({ label, color }: { label: string; color: string }) {
return (
<VStack
alignment="center"
modifiers={[fillFrame, background(color)]}>
<Spacer />
<Text
modifiers={[
font({ size: 28, weight: 'bold' }),
foregroundStyle('#FFFFFF'),
]}>
{label}
</Text>
<Spacer />
</VStack>
);
}
Controlled selection
Pass selection and onSelectionChange to drive the active tab from React state.
- Each
<TabView.Tab>'svalueis matched againstselection. - Add the 5 modifier to animate transitions when
selectionchanges from JS.
import { useState } from 'react';
import {
Button,
Host,
Spacer,
TabView,
Text,
VStack,
} from '@expo/ui/swift-ui';
import {
animation,
Animation,
background,
font,
foregroundStyle,
frame,
tabViewStyle,
} from '@expo/ui/swift-ui/modifiers';
const fillFrame = frame({
maxWidth: Infinity,
maxHeight: Infinity,
});
const pageFrame = frame({ minHeight: 320, maxHeight: 320 });
export default function ControlledTabViewExample() {
const [selected, setSelected] = useState('0');
return (
<Host style={{ flex: 1 }}>
<VStack>
<Text>Selected: {selected}</Text>
<Button
label="Go to page 3"
onPress={() => setSelected('2')}
/>
<TabView
selection={selected}
onSelectionChange={setSelected}
modifiers={[
pageFrame,
tabViewStyle({ type: 'page' }),
animation(Animation.default, Number(selected)),
]}>
<TabView.Tab value="0">
<Page label="Page 1" color="#6200EE" />
</TabView.Tab>
<TabView.Tab value="1">
<Page label="Page 2" color="#03DAC5" />
</TabView.Tab>
<TabView.Tab value="2">
<Page label="Page 3" color="#FF5722" />
</TabView.Tab>
</TabView>
</VStack>
</Host>
);
}
function Page({ label, color }: { label: string; color: string }) {
return (
<VStack
alignment="center"
modifiers={[fillFrame, background(color)]}>
<Spacer />
<Text
modifiers={[
font({ size: 28, weight: 'bold' }),
foregroundStyle('#FFFFFF'),
]}>
{label}
</Text>
<Spacer />
</VStack>
);
}
Page indicator dots
Use the indexViewStyle modifier together with tabViewStyle({ type: 'page' }) to control the dot indicators. Set indexDisplayMode to 'always', 'never', or 'automatic', and backgroundDisplayMode to render a translucent pill behind the dots.
import {
Host,
Spacer,
TabView,
Text,
VStack,
} from '@expo/ui/swift-ui';
import {
background,
font,
foregroundStyle,
frame,
indexViewStyle,
tabViewStyle,
} from '@expo/ui/swift-ui/modifiers';
const fillFrame = frame({
maxWidth: Infinity,
maxHeight: Infinity,
});
const pageFrame = frame({ minHeight: 320, maxHeight: 320 });
export default function PageIndicatorExample() {
return (
<Host style={{ flex: 1 }}>
<TabView
modifiers={[
pageFrame,
tabViewStyle({
type: 'page',
indexDisplayMode: 'always',
}),
indexViewStyle({ backgroundDisplayMode: 'always' }),
]}>
<TabView.Tab value="0">
<Page label="Page 1" color="#4F8DF6" />
</TabView.Tab>
<TabView.Tab value="1">
<Page label="Page 2" color="#34C759" />
</TabView.Tab>
<TabView.Tab value="2">
<Page label="Page 3" color="#FF9F0A" />
</TabView.Tab>
</TabView>
</Host>
);
}
function Page({ label, color }: { label: string; color: string }) {
return (
<VStack
alignment="center"
modifiers={[fillFrame, background(color)]}>
<Spacer />
<Text
modifiers={[
font({ size: 28, weight: 'bold' }),
foregroundStyle('#FFFFFF'),
]}>
{label}
</Text>
<Spacer />
</VStack>
);
}
Bottom tab bar
Use tabViewStyle({ type: 'automatic' }) for the SwiftUI default tab bar.
- Each tab's
labelandsystemImagepopulate the bar item. - Use the 3 modifier on a tab to attach a badge to its bar item.
Note: For routed bottom-tab navigation across full-screen routes, use
expo-router/unstable-native-tabsinstead.
import { useState } from 'react';
import {
Host,
Spacer,
TabView,
Text,
VStack,
} from '@expo/ui/swift-ui';
import {
background,
badge,
font,
foregroundStyle,
frame,
tabViewStyle,
} from '@expo/ui/swift-ui/modifiers';
const fillFrame = frame({
maxWidth: Infinity,
maxHeight: Infinity,
});
export default function BottomTabsExample() {
const [selected, setSelected] = useState('inbox');
return (
<Host style={{ flex: 1 }}>
<TabView
selection={selected}
onSelectionChange={setSelected}
modifiers={[tabViewStyle({ type: 'automatic' })]}>
<TabView.Tab
value="inbox"
label="Inbox"
systemImage="tray.fill"
modifiers={[badge('3')]}>
<Page label="Inbox" color="#4F8DF6" />
</TabView.Tab>
<TabView.Tab
value="sent"
label="Sent"
systemImage="paperplane.fill">
<Page label="Sent" color="#34C759" />
</TabView.Tab>
<TabView.Tab
value="drafts"
label="Drafts"
systemImage="square.and.pencil">
<Page label="Drafts" color="#FF9F0A" />
</TabView.Tab>
</TabView>
</Host>
);
}
function Page({ label, color }: { label: string; color: string }) {
return (
<VStack
alignment="center"
modifiers={[fillFrame, background(color)]}>
<Spacer />
<Text
modifiers={[
font({ size: 28, weight: 'bold' }),
foregroundStyle('#FFFFFF'),
]}>
{label}
</Text>
<Spacer />
</VStack>
);
}
API
import { TabView } from '@expo/ui/swift-ui';Component
TabView
Type: React.Element<TabViewProps>
A SwiftUI TabView. Pair with modifiers to choose the appearance:
tabViewStyle({ type: 'page' })— swipeable pager.tabViewStyle({ type: 'automatic' })— bottom tab bar.tabViewStyle({ type: 'sidebarAdaptable' })— sidebar on iPad, tab bar on iPhone.
Use <TabView.Tab> children to define pages. Each tab is identified by its value prop, which is used for selection.
For routed bottom-tab navigation across full-screen routes, prefer expo-router/unstable-native-tabs.
TabViewProps
children
Type: ReactNode
The tab's content — rendered when this tab is selected.
label
Optional • Type: string
Text label shown in the tab bar or sidebar.
systemImage
Optional • Type: SFSymbols7_0
SF Symbol name shown alongside the label.
value
Type: string
Identifies this tab. Matched against the parent TabView's selection and defaultSelection props.
Inherited props
children
Literal type: union
<TabView.Tab> elements defining the pages.
Acceptable values are: ReactElement<unknown, string | JSXElementConstructor<any>> | ReactElement[]
defaultSelection
Optional • Type: string
The initially selected tab when the component is uncontrolled (no selection prop). Ignored if selection is provided.
onSelectionChange
Optional • Type: (selection: string) => void
Called when the selected tab changes.
selection
Optional • Type: string
The selected tab (controlled mode). Pair with onSelectionChange. Pass defaultSelection instead to let the native view manage state.
Inherited props
Modifiers
Use these modifiers in the modifiers prop of the SwiftUI version.
Expo UI lets you put any of the 89 modifiers for any view on TabView, but not all of them do something here.
- 74 change what you see on TabView.
- 2 work, but not in the way that you may expect.
- 2 have no visible effect on TabView.
- 13 work only in one place, such as a widget or a
Grid.
Where the modifiers go
modifiers on TabView apply to the whole tab view. On iOS 18 and later, a TabView.Tab uses only its badge modifier and ignores all other modifiers. Put them on the tab content.
Best modifiers for TabView
These modifiers are made for TabView, or they are the ones that you use most with it.
Also works on TabView
TabView takes these modifiers too, and each one works in the usual way.
accessibilityAddTraitsaccessibilityElementaccessibilityHiddenaccessibilityHintaccessibilityIdentifieraccessibilityInputLabelsaccessibilityLabelaccessibilityRemoveTraitsaccessibilityValueallowsTighteninganimationaspectRatiobackgroundbackgroundOverlayblurboldborderbrightnessclippedclipShapecolorInvertcontainerRelativeFrameiOS 17+containerShapecontentShapecontentTransitioniOS 16+contrastcornerRadiusdisableddynamicTypeSizeenvironmentfixedSizefontforegroundColorforegroundStylegeometryGroupiOS 17+glassEffectgrayscalehiddenhueRotationidimageScaleitalickerninglineHeightiOS 26+lineLimitlineSpacingluminanceToAlphamaskminimumScaleFactormonospacedDigitmultilineTextAlignmentoffsetonGeometryChangeonLongPressGestureonTapGestureopacityoverlaypaddingredactedrotation3DEffectrotationEffectsaturationscaleEffectshadowstrikethroughstrokeBordertextCasetruncationModeunderline
Needs care on TabView
These modifiers work on TabView, but not in the way that you may expect.
| Modifier | What to know |
|---|---|
onAppear | Runs for the whole tab view, not for one tab. Put it on the content of a tab. |
onDisappear | Runs for the whole tab view, not for one tab. Put it on the content of a tab. |
No visible effect on TabView
TabView accepts these modifiers, but you see no change. They do not give an error.
| Modifier | Why nothing happens |
|---|---|
layoutPriority | It has an effect only when the view shares an axis with other views in a stack. |
zIndex | It changes the order of views that cover each other in the same parent, and TabView does not stack its children. |
Only in some places
These modifiers work only in a widget, in a Grid, in a Namespace or in a parent that asks for them.
| Modifier | Where it works |
|---|---|
activityBackgroundTint | Works only in a Live Activity. It has no effect in the app. |
alignmentGuide | Has an effect only when the parent, such as a VStack or ZStack, aligns its children by the same guide. |
containerBackground | Works only in a widget, or in a navigation container when you use a navigation placement. |
glassEffectId | Works only on a view that also has glassEffect, in a GlassEffectContainer and a Namespace. |
gridCellAnchor iOS 16+ | Works only on a cell of a Grid. Grid is in @expo/ui/swift-ui, but it has no Expo docs page. |
gridCellColumns | Works only on a view in a Grid.Row. Grid is in @expo/ui/swift-ui, but it has no Expo docs page. |
gridCellUnsizedAxes | Works only on a view in a Grid. Grid is in @expo/ui/swift-ui, but it has no Expo docs page. |
gridColumnAlignment iOS 16+ | Works only on a view in a Grid.Row. Grid is in @expo/ui/swift-ui, but it has no Expo docs page. |
invalidatableContent iOS 17+ | Has an effect only when a parent view uses redacted with the invalidated reason. |
matchedGeometryEffect | Works only in a Namespace. Give two views the same id and change between them with an animation. |
privacySensitive | Has an effect only when a parent view uses redacted with the privacy reason. |
unredacted | Has an effect only in a parent view that uses redacted. |
widgetURL | Works only in a widget. It has no effect in the app. |
Sources
The original Expo docs for this page:
- TabView (SwiftUI) (
source-of-truth/versions/v57.0.0/sdk/ui/swift-ui/tabview.md)



