::Product·Development

Expo UI is now stable: SwiftUI and Jetpack Compose from a single import

Starting with SDK 56, @expo/ui is stable. One import gives you SwiftUI on iOS and Jetpack Compose on Android. No JS reimplementation.

Kudo Chien

Kudo Chien

Engineering

Mathieu Acthernoene

Mathieu Acthernoene

Guest Author

Nishan Bende

Nishan Bende

Engineering

Vojtech Novak

Vojtech Novak

Engineering

Expo UI is now stable: SwiftUI and Jetpack Compose from a single import

Expo UI is now stable and ready for production. A single JS import gives you SwiftUI / Compose-backed primitive views for free. And it includes drop-in replacement views, so you can remove existing community packages from your package.json while you're at it.

Universal components in Expo UI, across Android, iOS and web.

Universal components in Expo UI, across Android, iOS and web.

Starting with SDK 56, @expo/ui lets you use SwiftUI on iOS and Jetpack Compose on Android. The real native primitives, not a JavaScript re-implementation.

Expo UI is in the default create-expo-app template and bundled into Expo Go, so you can try it the moment you create or open a project.

This milestone is the payoff of three SDK cycles of iteration:

  • SDK 53 — first prototype. A handful of leaf components for SwiftUI and Jetpack Compose.
  • SDK 54<Host>, SwiftUI modifiers, and container views like Form and List. The hot-chocolate showcase proved you could build a complete app this way.
  • SDK 55Jetpack Compose support in beta, and the SwiftUI surface was audited so every component, prop, and modifier name maps 1-to-1 with Apple's documentation.
  • SDK 56 — the Jetpack Compose surface got the same audit treatment, a universal layer sits on top of both, and a handful of long-standing community packages now have native drop-in replacements.

Here's how each piece lands in practice.

One import, all platforms: the universal components

The biggest new thing you can see in SDK 56 is the universal layer. Instead of importing from @expo/ui/swift-ui or @expo/ui/jetpack-compose, you can import from @expo/ui and get the platform-idiomatic implementation underneath:

import { Host, FieldGroup, Row, Switch, Slider, Text, Spacer } from '@expo/ui';

Under the hood, each universal component is just a wrapper: on iOS it renders the SwiftUI version, on Android it renders the Compose version. There's no JS re-implementation in between, so you're using the real platform component either way. Layout primitives, text, inputs, controls, and sheets are all in scope: Host, Row, Column, ScrollView, and more. Learn more about universal components.

A few naming choices worth calling out: it's Switch, not Toggle; it's Column / Row, not HStack / VStack. The universal layer leans toward names a React Native developer already types every day. When you want the SwiftUI-flavored name, it's still right there under @expo/ui/swift-ui.

Universal components showcase: settings screen

FieldGroup is the universal counterpart to SwiftUI's Form - a grouped, sectioned list with section titles, footers, and platform-appropriate styling on both iOS and Android. Note the compound components in the snippet below: FieldGroup.Section for sections, FieldGroup.SectionFooter for the gray descriptive text underneath.

import { useState } from 'react';
import { Button, FieldGroup, Host, Row, Slider, Spacer, Switch, Text } from '@expo/ui';
export default function SettingsScreen() {
const [notifications, setNotifications] = useState(true);
const [sounds, setSounds] = useState(false);
const [brightness, setBrightness] = useState(0.6);
return (
<Host style={{ flex: 1 }}>
<FieldGroup>
<FieldGroup.Section title="Notifications">
<LabeledRow label="Push notifications">
<Switch value={notifications} onValueChange={setNotifications} />
</LabeledRow>
<LabeledRow label="Sounds">
<Switch value={sounds} onValueChange={setSounds} />
</LabeledRow>
<FieldGroup.SectionFooter>
<Text textStyle={{ fontSize: 13, color: '#6c6c70' }}>
Notification previews can expose sensitive content on the lock screen.
</Text>
</FieldGroup.SectionFooter>
</FieldGroup.Section>
<FieldGroup.Section title="Display">
<LabeledRow label="Brightness">
<Slider value={brightness} onValueChange={setBrightness} />
</LabeledRow>
</FieldGroup.Section>
<FieldGroup.Section>
<Row alignment="center" style={{ padding: 12 }}>
<Spacer flexible />
<Button variant="outlined" onPress={() => alert('Signed out')} label="Sign out" />
<Spacer flexible />
</Row>
</FieldGroup.Section>
</FieldGroup>
</Host>
);
}
function LabeledRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<Row alignment="center" spacing={16}>
<Text>{label}</Text>
<Spacer flexible />
{children}
</Row>
);
}

Same code, both platforms. On iOS you get a SwiftUI Form with inset-grouped sections and a system Switch and Slider; on Android you get a Material 3 grouped list. Drop it into a screen and it looks like the rest of the OS (no .ios.tsx / .android.tsx split required).

The platform packages (@expo/ui/swift-ui, @expo/ui/jetpack-compose) are still there alongside the universal layer for the moments where a screen needs SwiftUI's glassEffect() or a Compose DockedSearchBar. Mix and match within the same Host.

Two early notes on the universal layer:
  • It's a first iteration. Some universal components work well on all three platforms; others don't, because SwiftUI, Compose, and the web don't always offer the same primitive. Tell us this SDK cycle: which universal components work for you, which ones don't, which ones are missing, and where you ended up using @expo/ui/swift-ui or @expo/ui/jetpack-compose directly.
  • Web is experimental. Universal components have web implementations, but they're not at the same quality bar as native yet. Expect more iteration in upcoming SDKs as we shape them with your feedback.

Stable native APIs and new features

With both audits done, most SwiftUI or Compose samples you find (Apple docs, Google docs, a Medium post, an LLM's training data) are roughly one search-and-replace away from working in Expo UI. Component name, prop name, modifier name, all the same.

Concretely: SwiftUI's Toggle and Form keep their names in @expo/ui/swift-ui; Compose's LazyColumn keeps its name in @expo/ui/jetpack-compose. Prop and modifier names follow the same rule.

SDK 56 includes the final round of renames from the Compose audit. From this release forward, the API is mostly stable.

Alongside the audit, a handful of new features shipped this cycle:

  • Extensibility. SwiftUI and Jetpack Compose ship hundreds of views and modifiers — far more than we'll ever wrap in @expo/ui. To close that gap, you can now build your own SwiftUI and Jetpack Compose views and modifiers, and Expo UI handles the layout, prop, and event plumbing. Learn more for SwiftUI and Jetpack Compose.
  • Material 3 Dynamic Colors. useMaterialColors hands you Material 3 Dynamic Colors that follow the system theme.
  • Material Symbols catalog. The Icon component pairs with @expo/material-symbols to bring the full Material Symbols catalog within import reach.
  • Worklet-driven native state. useNativeState for SwiftUI and Jetpack Compose lets you build flicker-free, UI-thread-driven controls. We'll have a follow-up post dedicated to that story.

Drop-in replacements for community packages

Most React Native apps end up with the same little stack of community packages for platform primitives like pickers or sliders. Each is its own native dependency…another package to install, another release schedule to track, another thing that can break on an SDK bump. SDK 56 ships native replacements for seven of them under @expo/ui/community:

Community package Drop-in replacement
@react-native-community/datetimepicker @expo/ui/community/datetime-picker
@react-native-community/slider @expo/ui/community/slider
react-native-pager-view @expo/ui/community/pager-view
@react-native-picker/picker@expo/ui/community/picker
@react-native-segmented-control/segmented-control @expo/ui/community/segmented-control
@react-native-masked-view/masked-view @expo/ui/community/masked-view
@react-native-menu/menu @expo/ui/community/menu
@gorhom/bottom-sheet @expo/ui/community/bottom-sheet

Each replacement is near-API-compatible with the package it replaces. Most migrations are a one-line import swap, though some props differ because Expo UI is backed by SwiftUI and Jetpack Compose rather than UIKit and Android Views:

// Before
import DateTimePicker from '@react-native-community/datetimepicker';
// After
import DateTimePicker from '@expo/ui/community/datetime-picker';

Learn more about drop-in replacements.

Net effect: fewer native dependencies, one upgrade story, the same audited foundation underneath.

Pick the right tool for each part of your app

You write <div> and <span> in react-dom; you write Column and Row in @expo/ui. Same idea: Expo UI isn't a UI component library or a fancy design system. It just exposes the primitives iOS and Android already ship, as React components.

But Expo UI isn't the only tool in the box. One of the things that makes Expo work as a framework is that you can mix toolkits inside a single app:

  • For custom UI with your own design — anything from a brand page to a fully bespoke design system — use React Native View / Text. They're styling-agnostic primitives where CSS-style props, flexbox layout, and styling libraries like NativeWind all work.
  • For UI that should feel native — settings, modals, pickers, sheets, anywhere your users expect "this looks like the OS" — reach for Expo UI. Inside a Host, layout is SwiftUI / Compose primitives (HStack/VStack, Row/Column), not Yoga flexbox.
  • For everything-drawn-yourself work — charts, custom shaders, bespoke animations — use react-native-skia.
  • For 3D and GPU compute — use react-native-webgpu and TypeGPU.
  • For screens where the web ecosystem already has the answer (e.g. shadcn) — use DOM components to run web code in a webview, on native and as-is on web.

Most importantly, these options compose at the component level. You can mix and match React Native, Expo UI, Skia, WebGPU / TypeGPU, and DOM components inside the same screen and the same view tree; building experiences that are universal and deeply native at the same time.

Updated showcase apps

We've updated hot-chocolate to SDK 56. The app was originally SwiftUI-only. Now it has real universal components and it also runs on Android and web.

Apple TV and Android TV

Expo UI now runs on both Apple TV and Android TV. This is thanks in large part to the work from Douglas Lowder. Most components and APIs are supported; except some native SwiftUI and Compose APIs simply aren't available on TV. The ExpoUITV app shows nearly all the supported APIs and runs on both TV and mobile.

Thanks to the community

Expo UI wouldn't be at stable without you. The SDK 56 cycle was unusually community-driven. You all filed issues, audited APIs alongside us, added entire components, and shipped fixes for crashes we hadn't even seen yet. Special thanks to:

2hwayoung, AKSHAY JADHAV, Axel, Benjamin Komen, Beto, Christian Wooldridge, Dennis Morello, Dylan, Eliot Gevers, fedeciancaglini, Gregory Moskaliuk, Hugo Extrat, hypnokermit, Ian Berry, Isaiah Hamilton, JeroenG, Joss Mackison, K.Dileepa Thushan Peiris, Kfir Fitousi, kimchi-developer, Leonardo E. Dominguez, Liès, Loic CHOLLIER, Louis, lucabc2000, Petr Chalupa, Pflaumenbaum, Ray, Sam Shubham, Shubh Porwal, starsky-nev, Suvesh Moza, Terijaki, ThiMal, Yousof Abouhalawa, Zhovtonizhko Dmitriy

And to everyone who tested, filed issues, joined office hours, or just hung out in Discord while we worked through SDK 56, thank you. You shaped this release just as much as the PRs did.

Where to start

  1. npx create-expo-app@latest --template default@sdk-56 — Expo UI is in the default template, so Host, Switch, and Picker are ready to import.
  2. If your existing app has any of the packages in the drop-in table above, try swapping a single import.
  3. Browse the showcase apps: hot-chocolate and ExpoUITV.
  4. File issues, send PRs, talk to us in Discord. Stable doesn't mean done — it means the foundation is firm enough for the next round of building.

Welcome to stable Expo UI. Now go build something native.

Expo UI

Share article