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

SwiftUI

SwiftUI components for building native iOS interfaces with @expo/ui.

The SwiftUI components in @expo/ui/swift-ui allow you to build fully native iOS interfaces using SwiftUI from React Native.

Installation

bun expo install @expo/ui

If you are installing this in an existing React Native app, make sure to install expo in your project.

Usage

Using a component from @expo/ui/swift-ui requires wrapping it in a Host component. The Host is a container for SwiftUI views.

import { Host, Button } from '@expo/ui/swift-ui';

export function SaveButton() {
  return (
    <Host style={{ flex: 1 }}>
      <Button label="Save changes" />
    </Host>
  );
}

For more information, see the following resources:

Expo UI guide for Swift UI — Learn about the basics of @expo/ui/swift-ui — @expo/ui/swift-ui

Extending with SwiftUI — Create custom SwiftUI components and modifiers that integrate with Expo UI.

Expo UI iOS Liquid Glass Tutorial — Learn how to build real SwiftUI views in your React Native app with the new Expo UI.

Available components

ComponentDescription
AccessoryWidgetBackgroundAdaptive background view that provides a standard appearance based on the widget's environment.
AlertAlert component for presenting native iOS alert dialogs.
BottomSheetBottomSheet component that presents content from the bottom of the screen.
ButtonButton component for displaying native buttons.
ColorPickerColorPicker component for selecting colors.
ConfirmationDialogConfirmationDialog component for presenting confirmation prompts.
ContextMenuContextMenu component for displaying context menus.
ControlGroupControlGroup component for grouping interactive controls.
DatePickerDatePicker component for selecting dates and times.
DisclosureGroupDisclosureGroup component for displaying expandable content.
DividerDivider component for creating visual separators.
FormForm component for collecting user input in a structured layout.
GaugeGauge component for displaying progress with visual indicators.
GroupGroup component for grouping views without affecting layout.
HostHost component that enables SwiftUI components in React Native.
HStackHStack component for horizontal layouts.
ImageImage component for displaying SF Symbols.
LabelLabel component for displaying text with an icon.
LazyHStackLazyHStack component for lazy horizontal layouts.
LazyVStackLazyVStack component for lazy vertical layouts.
LinkLink component for displaying clickable links.
ListList component for displaying scrollable lists of items.
MenuMenu component for displaying dropdown menus.
ModifiersView modifiers for customizing component appearance and behavior.
NamespaceA Namespace component that allows you create Namespaces in SwiftUI
OverlayOverlay component for layering content on top of another view.
PickerPicker component for selecting options from a list.
PopoverPopover component for displaying content in a floating overlay.
ProgressViewProgressView component for displaying progress indicators.
RNHostViewA component that enables React Native views inside SwiftUI.
ScrollViewScrollView component for scrollable content.
SectionSection component for grouping content within lists and forms.
SecureFieldSecureField component for password input.
SliderSlider component for selecting values from a range.
SpacerSpacer component for flexible spacing.
SwipeActionsSwipeActions component for adding leading and trailing swipe actions to row content.
TabViewTabView component for paged or tabbed content.
TextText component for displaying styled text with support for nested texts.
TextFieldTextField component for text input.
ToggleToggle component for displaying native toggles.
useNativeStateA React hook that creates observable state shared between JavaScript and native SwiftUI views.
VStackVStack component for vertical layouts.
ZStackZStack component for overlapping layouts.

Build a SwiftUI screen

Available in SDK 54 and later.

Expo UI brings SwiftUI to React Native. You can use modern SwiftUI primitives to build your apps.

This guide covers the basics of using Expo UI to integrate SwiftUI into your Expo apps.

Expo UI iOS Liquid Glass Tutorial — Learn how to build real SwiftUI views in your React Native app with the new Expo UI.

Features

  • SwiftUI primitives: Expo UI is not another UI library. It brings SwiftUI primitives to Expo.
  • 1-to-1 mapping: The components in Expo UI have a 1-to-1 mapping to SwiftUI views. You can easily explore available views in the SwiftUI ecosystem, such as Explore SwiftUI or the Libraried app, and find the corresponding Expo UI component.
  • Full-app support: Expo UI is designed to be used throughout the entire app. You can write your app entirely in Expo UI, while maintaining flexibility at the same time. The integration works at the component level. You can also mix React Native components, Expo UI components, DOM components, or custom 2D components using react-native-skia.

Installation

You'll need to install the @expo/ui package in your Expo project. Run the following command to install it:

npx expo install @expo/ui

Expo Skills for AI agents

If you use an AI agent, install Expo Skills to teach it how to build native-feeling screens:

expo-ui — Build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android.

expo-native-ui — Build beautiful, native-feeling Expo screens.

Usage

Expo UI has several SwiftUI components available.

  • You can use them in your app by importing them from @expo/ui/swift-ui.
  • However, to cross the boundary from React Native (UIKit) to SwiftUI, you need to use the 1 component.
  • The 2 is the container for SwiftUI views.
  • You can think of it like 3 in the DOM or 4 in 5.
  • Under the hood, it uses 6 to render SwiftUI views in UIKit.

Basic usage with Host

Code
import { CircularProgress, Host } from '@expo/ui/swift-ui';
import { View, Text } from 'react-native';

export default function LoadingView() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Host matchContents>
        <CircularProgress />
      </Host>
      <Text>Loading...</Text>
    </View>
  );
}
Preview

Using HStack and VStack

You can also use the HStack and VStack components to build the entire layout in SwiftUI.

Code
import { CircularProgress, Host, HStack, LinearProgress, VStack } from '@expo/ui/swift-ui';

export default function LoadingView() {
  return (
    <Host style={{ flex: 1, margin: 32 }}>
      <VStack spacing={32}>
        <HStack spacing={32}>
          <CircularProgress />
          <CircularProgress color="orange" />
        </HStack>
        <LinearProgress progress={0.5} />
        <LinearProgress color="orange" progress={0.7} />
      </VStack>
    </Host>
  );
}
Preview

Modifiers

SwiftUI modifier is a powerful way to customize the appearance and behavior of SwiftUI components.

  • Expo UI also provides modifiers for SwiftUI components.
  • You can import modifiers from @expo/ui/swift-ui/modifiers and pass them as an array to the modifiers prop.
  • In the following example, the 2 and glassEffect modifier are combined to create Liquid Glass text.
Code

Note: glassEffect modifier requires Xcode 26+ and iOS 26+.

import { Host, Text } from '@expo/ui/swift-ui';
import { glassEffect, padding } from '@expo/ui/swift-ui/modifiers';
import { MeshGradientView } from 'expo-mesh-gradient';
import { View } from 'react-native';

export default function Page() {
  return (
    <View style={{ flex: 1 }}>
      <MeshGradientView
        style={{ flex: 1 }}
        columns={3}
        rows={3}
        colors={['red', 'purple', 'indigo', 'orange', 'white', 'blue', 'yellow', 'green', 'cyan']}
        points={[
          [0.0, 0.0],
          [0.5, 0.0],
          [1.0, 0.0],
          [0.0, 0.5],
          [0.5, 0.5],
          [1.0, 0.5],
          [0.0, 1.0],
          [0.5, 1.0],
          [1.0, 1.0],
        ]}
      />
      <Host style={{ position: 'absolute', top: 0, right: 0, left: 0, bottom: 0 }}>
        <Text
          size={32}
          modifiers={[
            padding({
              all: 16,
            }),
            glassEffect({
              glass: {
                variant: 'clear',
              },
            }),
          ]}>
          Glass effect text
        </Text>
      </Host>
    </View>
  );
}
Preview

iOS Settings app example

Combining the Expo UI components and modifiers, you can build a UI like iOS Settings app.

Code
import {
  Button,
  Form,
  Host,
  HStack,
  Image,
  Section,
  Spacer,
  Toggle,
  Text,
} from '@expo/ui/swift-ui';
import { background, buttonStyle, foregroundStyle, clipShape, frame } from '@expo/ui/swift-ui/modifiers';
import { Link } from 'expo-router';
import { useState } from 'react';

export default function SettingsView() {
  const [isAirplaneMode, setIsAirplaneMode] = useState(true);

  return (
    <Host style={{ flex: 1 }}>
      <Form>
        <Section>
          <HStack spacing={8}>
            <Image
              systemName="airplane"
              color="white"
              size={18}
              modifiers={[
                frame({ width: 28, height: 28 }),
                background('#ffa500'),
                clipShape('roundedRectangle'),
              ]}
            />
            <Text>Airplane Mode</Text>
            <Spacer />
            <Toggle isOn={isAirplaneMode} onIsOnChange={setIsAirplaneMode} />
          </HStack>

          <Link href="/wifi" asChild>
            {/* Use buttonStyle('plain') to prevent default blue button styling */}
            <Button modifiers={[buttonStyle('plain')]}>
              <HStack spacing={8}>
                <Image
                  systemName="wifi"
                  color="white"
                  size={18}
                  modifiers={[
                    frame({ width: 28, height: 28 }),
                    background('#007aff'),
                    clipShape('roundedRectangle'),
                  ]}
                />
                {/* When Text is wrapped in a Link, the color needs to be specified explicitly */}
                <Text modifiers={[foregroundStyle({type: 'color', color: 'black'})]}>Wi-Fi</Text>
                <Spacer />
                <Image systemName="chevron.right" size={14} color="secondary" />
              </HStack>
            </Button>
          </Link>
        </Section>
      </Form>
    </Host>
  );
}
Preview

Secondary text styling

Use foregroundStyle to apply a hierarchical style, which will make text appear lighter and more subtle.

Code
import { Button, Form, Host, HStack, Image, List, Section, Spacer, Text } from '@expo/ui/swift-ui';
import { buttonStyle, font, foregroundStyle, padding } from '@expo/ui/swift-ui/modifiers';

export default function SecondaryTextExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Form>
        <Section>
          <List>
            <Button onPress={() => console.log('Navigate')} modifiers={[buttonStyle('plain')]}>
              <HStack>
                <Text>Night Shift</Text>
                <Spacer />
                <Text
                  modifiers={[
                    foregroundStyle({type: 'hierarchical', style: 'secondary'}),
                    padding({ trailing: 8 }),
                  ]}>
                  22:00 to 07:00
                </Text>
                <Image systemName="chevron.right" size={14} color="#C7C7CC" />
              </HStack>
            </Button>
          </List>
          <List>
            <Text modifiers={[foregroundStyle({type: 'hierarchical', style: 'secondary'}), font({ size: 14 })]}>
              Save up to 280.7 MB. This will permanently delete all photos and videos kept in the
              "Recently Deleted" album.
            </Text>
          </List>
        </Section>
      </Form>
    </Host>
  );
}
Preview

Slider with icons

A common pattern for brightness or volume controls is to flank a Slider with icons.

Code
import { useState } from 'react';
import {
  Form,
  Host,
  HStack,
  Image,
  List,
  Section,
  Slider,
  Spacer,
  Text,
  Toggle,
} from '@expo/ui/swift-ui';
import { padding } from '@expo/ui/swift-ui/modifiers';

export default function SliderWithIconsExample() {
  const [brightness, setBrightness] = useState(0.5);
  const [trueToneEnabled, setTrueToneEnabled] = useState(true);

  return (
    <Host style={{ flex: 1 }}>
      <Form>
        <Section
          header={<Text>Brightness</Text>}
          footer={
            <Text>
              Automatically adapt iPhone display based on ambient lighting
              conditions to make colors appear consistent in different
              environments.
            </Text>
          }
        >
          <List>
            <HStack modifiers={[padding({ vertical: 6 })]}>
              <Image systemName="sun.min.fill" size={22} color="#8E8E93" />
              <Spacer />
              <Slider value={brightness} onValueChange={setBrightness} />
              <Spacer />
              <Image systemName="sun.max.fill" size={22} color="#8E8E93" />
            </HStack>
            <Toggle
              label="True Tone"
              isOn={trueToneEnabled}
              onIsOnChange={setTrueToneEnabled}
            />
          </List>
        </Section>
      </Form>
    </Host>
  );
}
Preview

Multi-line list items

Use VStack with alignment="leading" for list items with title and subtitle.

Code
import {
  Button,
  Form,
  Host,
  HStack,
  Image,
  List,
  Section,
  Spacer,
  Text,
  VStack,
} from '@expo/ui/swift-ui';
import { buttonStyle, font, foregroundStyle, padding } from '@expo/ui/swift-ui/modifiers';

export default function MultiLineListItemExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Form>
        <Section>
          <List>
            <HStack>
              <Image
                systemName="safari"
                size={22}
                modifiers={[padding({ trailing: 6 })]}
              />
              <Spacer />
              <Button
                onPress={() => console.log('Navigate')}
                modifiers={[buttonStyle('plain'), padding({ vertical: 6 })]}
              >
                <VStack spacing={4} alignment="leading">
                  <Text>Chrome</Text>
                  <Text modifiers={[foregroundStyle({type: 'hierarchical', style: 'secondary'}), font({ size: 14 })]}>
                    Last used: Today
                  </Text>
                </VStack>
                <Spacer />
                <Text
                  modifiers={[
                    foregroundStyle({type: 'hierarchical', style: 'secondary'}),
                    font({ size: 16 }),
                  ]}
                >
                  1.57 GB
                </Text>
                <Image systemName="chevron.right" size={14} color="#C7C7CC" />
              </Button>
            </HStack>
          </List>
        </Section>
      </Form>
    </Host>
  );
}
Preview

Common questions

Can I use flexbox or other styles in SwiftUI components?

Flexbox styles can be applied to the Host component itself. Once you're inside the SwiftUI context, however, Yoga is not available — layouts should be defined using <HStack> and <VStack> instead.

What's the Host component?

Host is the container for SwiftUI views.

  • You can think of it like 1 in the DOM or 2 in 3.
  • Under the hood, it uses 4 to render SwiftUI views in UIKit.
How is Expo UI different from libraries like react-native-paper or react-native-elements?

Expo UI is not "yet another" UI library and not an opinionated design kit. Instead, it's a primitives library. It exposes native SwiftUI and Jetpack Compose components directly to JavaScript, rather than re-implementing or simulating UI in JavaScript.

Can I use React Native components inside SwiftUI components?

Yes, you can place React Native components as JSX children of Expo UI components.

  • Expo UI automatically creates a 0 wrapper for you.
  • However, keep in mind that the SwiftUI layout system works differently from UIKit and has some limitations.
  • According to Apple's documentation:

SwiftUI fully controls the layout of the UIKit view's center, bounds, frame, and transform properties. Don't directly set these layout-related properties on the view managed by a UIViewRepresentable instance from your own code because that conflicts with SwiftUI and results in undefined behavior.

Also note that once you render React Native components, you're leaving the SwiftUI context. If you want to add Expo UI components again, you'll need to reintroduce a Host wrapper.

We recommend keeping SwiftUI layouts self-contained. Interop is possible, but it works best when boundaries are clearly defined.

I'm a SwiftUI developer. Why should I learn Expo UI?

Because React's promise of "learn once, write anywhere", it now extends to SwiftUI and Jetpack Compose.

  • With Expo UI, you can apply your SwiftUI knowledge to build apps that run in the React Native ecosystem, extend to the Web through DOM components, and even integrate 2D and 3D rendering.
  • The system is flexible enough that different parts of your app can use different approaches — giving you seamless integration at the component level.

Additional resources

Expo UI reference — For information on API components, methods, and more, see the Expo UI reference.

Expo UI example — Our latest Expo UI examples.

Hot Chocolate app example — An example app replicating the YVR Hot Chocolate Fest app with Expo UI.

Expo UI multiplatform demo — A project demonstrating the production-ready Expo UI package available in SDK 56 and later.

  • The project works on both TV (Android TV, Apple TV) and mobile (Android, iOS).
  • Demo screens include most of the available components for both Jetpack Compose and Swift UI.

Add your own SwiftUI views

This guide explains how to create custom SwiftUI components and modifiers that integrate seamlessly with Expo UI.

Prerequisites
@expo/ui installed

Install @expo/ui in your project. See Building SwiftUI apps with Expo UI for more information.

npx expo install @expo/ui
A development build

Expo UI is not available in Expo Go. Create a development build of your app.

Familiarity with Expo Modules API and SwiftUI

Basic familiarity with Expo Modules API and SwiftUI is recommended.

Creating a custom component

Project setup

Create a local Expo module in your project:

npx create-expo-module@latest --local my-ui

Add ExpoUI as a dependency in your module's podspec file:

Pod::Spec.new do |s|
  s.name           = 'MyUi'
  s.version        = '1.0.0'
  s.summary        = 'Custom UI components extending Expo UI'
  # ... other config
  # Add ExpoUI dependency
  s.dependency 'ExpoUI'
  # ... other config
end

Creating a SwiftUI view

Create your SwiftUI view with two parts:

  1. Props class: Extends UIBaseViewProps from ExpoUI to get automatic support for the modifiers prop
  2. View struct: Conforms to ExpoSwiftUI.View protocol, which requires an @ObservedObject props property and a body
import SwiftUI
import ExpoModulesCore
import ExpoUI

final class MyCustomViewProps: UIBaseViewProps {
  @Field var title: String = ""
}

struct MyCustomView: ExpoSwiftUI.View {
  @ObservedObject public var props: MyCustomViewProps

  var body: some View {
    VStack {
      Text(props.title)
        .font(.headline)
      Children() // Renders React children
    }
  }
}

Register the view in your module using ExpoUIView. This wraps your SwiftUI view with modifier support and makes it available to JavaScript:

import ExpoModulesCore
import ExpoUI

public class MyUiModule: Module {
  public func definition() -> ModuleDefinition {
    Name("MyUi")

    ExpoUIView(MyCustomView.self)
  }
}

Create a wrapper component that connects modifiers with event handling. The createViewModifierEventListener utility enables event-based modifiers like onTapGesture and onAppear to work with your custom view:

import { requireNativeView } from 'expo';
import { type CommonViewModifierProps } from '@expo/ui/swift-ui';
import { createViewModifierEventListener } from '@expo/ui/swift-ui/modifiers';

export interface MyCustomViewProps extends CommonViewModifierProps {
  title: string;
  children?: React.ReactNode;
}

const NativeMyCustomView = requireNativeView<MyCustomViewProps>('MyUi', 'MyCustomView');

export function MyCustomView({ modifiers, ...restProps }: MyCustomViewProps) {
  return (
    <NativeMyCustomView
      modifiers={modifiers}
      {...(modifiers ? createViewModifierEventListener(modifiers) : undefined)}
      {...restProps}
    />
  );
}

Using your custom component

Your custom component now works with all ExpoUI built-in modifiers:

import { Host, Text } from '@expo/ui/swift-ui';
import { padding, cornerRadius, background } from '@expo/ui/swift-ui/modifiers';
import { MyCustomView } from './modules/my-ui';

export default function App() {
  return (
    <Host style={{ flex: 1 }}>
      <MyCustomView
        title="Hello World"
        modifiers={[padding({ all: 16 }), cornerRadius(12), background('#f0f0f0')]}>
        <Text>Child content</Text>
      </MyCustomView>
    </Host>
  );
}

Creating custom modifiers

You can also create custom modifiers that work with any Expo UI component.

Modifiers are SwiftUI's way to configure views for styling, layout, behavior, and more. Learn more in Apple's ViewModifier documentation.

Native modifier implementation

Create a modifier struct that conforms to ViewModifier and Record:

import SwiftUI
import ExpoModulesCore
import ExpoUI

struct CustomBorderModifier: ViewModifier, Record {
  @Field var color: Color = .red
  @Field var width: CGFloat = 2
  @Field var cornerRadius: CGFloat = 0

  func body(content: Content) -> some View {
    content
      .overlay(
        RoundedRectangle(cornerRadius: cornerRadius)
          .stroke(color, lineWidth: width)
      )
  }
}

Register your modifier with ViewModifierRegistry in your module definition. Use OnCreate to register and OnDestroy to unregister to avoid race conditions with the SwiftUI render thread:

import ExpoModulesCore
import ExpoUI

public class MyUiModule: Module {
  public func definition() -> ModuleDefinition {
    Name("MyUi")

    OnCreate {
      ViewModifierRegistry.register("customBorder") { params, appContext, _ in
        return try CustomBorderModifier(from: params, appContext: appContext)
      }
    }

    OnDestroy {
      ViewModifierRegistry.unregister("customBorder")
    }

    ExpoUIView(MyCustomView.self)
  }
}

JavaScript modifier function

Create a TypeScript function that generates the modifier config:

import { createModifier } from '@expo/ui/swift-ui/modifiers';

export const customBorder = (params: { color?: string; width?: number; cornerRadius?: number }) =>
  createModifier('customBorder', params);

Export the modifier from your module:

export { MyCustomView, type MyCustomViewProps } from './src/MyCustomView';
export { customBorder } from './src/modifiers';

Using custom modifiers

Your custom modifier works with any ExpoUI component:

import { Host, Text, VStack } from '@expo/ui/swift-ui';
import { padding } from '@expo/ui/swift-ui/modifiers';
import { customBorder } from './modules/my-ui';

export default function App() {
  return (
    <Host style={{ flex: 1 }}>
      <VStack
        modifiers={[
          padding({ all: 20 }),
          customBorder({ color: '#FF6B35', width: 3, cornerRadius: 8 }),
        ]}>
        <Text>This has a custom border!</Text>
      </VStack>
    </Host>
  );
}

Next steps

Congratulations! You've learned how to extend Expo UI with custom SwiftUI components and modifiers. Your custom components now integrate seamlessly with the built-in modifier system.

Here are some ideas for what to build next:

  • Use the built-in SwiftUI components that come with Expo UI
  • Build custom modifiers for app-specific styling patterns
  • Wrap third-party SwiftUI libraries for use in React Native
  • Share your components as an npm package for others to use

Sources

The original Expo docs for this page:

On this page