Expo UI and Router for iOSExpo SDK 57 and later, iOS only
Components

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-tabs instead.

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>
  );
}
A teal pager page labeled Page 2, with three dot indicators and the middle dot active
A teal pager page labeled Page 2, with three dot indicators and the middle dot active

Controlled selection

Pass selection and onSelectionChange to drive the active tab from React state.

  • Each <TabView.Tab>'s value is matched against selection.
  • Add the 5 modifier to animate transitions when selection changes 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>
  );
}
A Selected 0 readout and a Go to page 3 button above a purple pager page labeled Page 1
A Selected 0 readout and a Go to page 3 button above a purple pager page labeled Page 1

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>
  );
}
A blue pager page labeled Page 1 with three dots inside a translucent pill
A blue pager page labeled Page 1 with three dots inside a translucent pill

Bottom tab bar

Use tabViewStyle({ type: 'automatic' }) for the SwiftUI default tab bar.

  • Each tab's label and systemImage populate 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-tabs instead.

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>
  );
}
A blue Inbox page above a tab bar with Inbox, Sent, and Drafts, where Inbox carries a red badge reading 3
A blue Inbox page above a tab bar with Inbox, Sent, and Drafts, where Inbox carries a red badge reading 3

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.

Needs care on TabView

These modifiers work on TabView, but not in the way that you may expect.

ModifierWhat to know
onAppearRuns for the whole tab view, not for one tab. Put it on the content of a tab.
onDisappearRuns 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.

ModifierWhy nothing happens
layoutPriorityIt has an effect only when the view shares an axis with other views in a stack.
zIndexIt 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.

ModifierWhere it works
activityBackgroundTintWorks only in a Live Activity. It has no effect in the app.
alignmentGuideHas an effect only when the parent, such as a VStack or ZStack, aligns its children by the same guide.
containerBackgroundWorks only in a widget, or in a navigation container when you use a navigation placement.
glassEffectIdWorks 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.
gridCellColumnsWorks only on a view in a Grid.Row. Grid is in @expo/ui/swift-ui, but it has no Expo docs page.
gridCellUnsizedAxesWorks 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.
matchedGeometryEffectWorks only in a Namespace. Give two views the same id and change between them with an animation.
privacySensitiveHas an effect only when a parent view uses redacted with the privacy reason.
unredactedHas an effect only in a parent view that uses redacted.
widgetURLWorks only in a widget. It has no effect in the app.

Sources

The original Expo docs for this page:

On this page