Expo UI and Router for iOS
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>
  );
}

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 animation 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>
  );
}

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 label and systemImage populate the bar item. Use the badge 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>
  );
}

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.

Modifiers for TabView

Modifiers for any view

Sources

The original Expo docs for this page:

On this page