For years, React Native apps re-created native controls in JavaScript: switches, sliders, pickers, and settings lists that were almost native. Expo UI (@expo/ui) takes a different approach. It renders the real SwiftUI views on iOS and real Jetpack Compose views on Android from React components.
The SwiftUI and Compose APIs became stable in SDK 56, and SDK 58 added recycling lists and native navigation components. If you're building settings screens, forms, or anything that should feel exactly like the OS, Expo UI is now the first thing to reach for.
Install
npx expo install @expo/ui
Expo UI is included in Expo Go, so you can try it immediately. It works the same in development and production builds.
Three ways to import
@expo/uiis the universal API. One component maps to SwiftUI on iOS and Compose on Android. Start here.@expo/ui/swift-uigives you SwiftUI-named components (Toggle,Form,List) when you want iOS-specific behavior.@expo/ui/jetpack-composegives you Compose-named components (LazyColumn, and so on) for Android-specific UI.
You can mix universal and platform-specific components inside the same Host.
Your first component: a native Switch
Every Expo UI component must live inside a Host, which bridges React Native's view tree to the native UI tree:
import { useState } from "react"
import { Host, Switch } from "@expo/ui"
export function NotificationsToggle() {
const [enabled, setEnabled] = useState(true)
return (
<Host matchContents={{ vertical: true }} style={{ width: "100%" }}>
<Switch
label="Enable notifications"
value={enabled}
onValueChange={setEnabled}
/>
</Host>
)
}
matchContents tells the Host to size itself to the native content. Flexbox styles (style) apply to the Host, not to the SwiftUI or Compose view inside it.
Buttons with native variants
The universal Button supports three variants that map to each platform's own styles:
import { Button, Host } from "@expo/ui"
<Host matchContents>
<Button variant="filled" label="Continue" onPress={onContinue} />
</Host>
filled(the default) has a solid background.outlinedhas a transparent background with a border.textshows the label only.
Pass children instead of label when you need custom content inside the button.
Build a native settings screen
The most popular use for Expo UI is the settings screen. FieldGroup gives you grouped sections that look like iOS Settings on iPhone and follow Material on Android:
import { useState } from "react"
import { FieldGroup, Host, Slider, Switch, Text } from "@expo/ui"
export default function SettingsScreen() {
const [notifications, setNotifications] = useState(true)
const [haptics, setHaptics] = useState(true)
const [volume, setVolume] = useState(0.6)
return (
<Host style={{ flex: 1 }}>
<FieldGroup>
<FieldGroup.Section title="Notifications">
<Switch label="Push notifications" value={notifications} onValueChange={setNotifications} />
<Switch label="Haptic feedback" value={haptics} onValueChange={setHaptics} />
</FieldGroup.Section>
<FieldGroup.Section title="Sound">
<Text>Volume</Text>
<Slider value={volume} onValueChange={setVolume} />
</FieldGroup.Section>
</FieldGroup>
</Host>
)
}
That's a fully native settings screen. Dynamic Type, dark mode, VoiceOver and TalkBack, and platform spacing come for free.
Universal components you can use today
The universal layer covers the building blocks most screens need:
- Layout:
Host,Row,Column,ScrollView,Spacer - Forms:
FieldGroup,FieldGroup.Section,FieldGroup.SectionFooter - Controls:
Switch,Slider,Button - Content:
Text
The platform-specific packages go much further: date pickers, menus, sheets, lists, and (new in SDK 58) NavigationStack, Toolbar, and NavigationLink on iOS.
Modifiers: the SwiftUI and Compose way to style
Platform components accept a modifiers prop. Modifier names follow Apple's and Google's docs, so the SwiftUI and Compose references apply directly. This is also how you access platform-only effects such as SwiftUI's glassEffect() for Liquid Glass. For a no-SwiftUI way to add glass to normal views, see our Liquid Glass and glassmorphism guide.
Recycling lists in SDK 58
Before SDK 58, Expo UI lists rendered every row. Now List, LazyVStack, LazyHStack, LazyColumn, and LazyRow virtualize and recycle rows, so you can use them for long, data-driven lists. For very large or highly custom feeds, FlashList v2 is still the more flexible choice.
Expo UI vs regular React Native components
Use Expo UI when:
- The screen should feel exactly like the OS (settings, forms, preferences, pickers).
- Accessibility and platform conventions matter more than custom branding.
- You want new OS design changes, like Liquid Glass, without rewriting anything.
Use React Native views (and NativeWind) when:
- You need a custom, branded look that's identical on both platforms.
- You're building bespoke interactions such as gesture-driven sheets, animated pickers, or confetti.
- You need full web parity today (Expo UI web is still experimental).
Most production apps use both: Expo UI for system-style screens, and custom components for the brand moments.
Custom components that pair with Expo UI
Our free, MIT-licensed components cover the custom side: a gesture-driven bottom sheet, a curved arc wheel picker, and an animated bottom navigation bar. Browse everything in the component library, and see what else shipped in Expo SDK 58.