expo-ui
Build native UI with @expo/ui: real SwiftUI on iOS and Jetpack Compose on Android. Use for sheets, pickers, sliders, toggles, menus and grouped form sections, with drop-in replacements for React Native community libraries.
Install
npx skills add https://github.com/expo/skills --skill expo-uiPlatform-specific Android UI: @expo/ui/jetpack-compose
Android only. Code that imports from
@expo/ui/jetpack-composewill crash on iOS with "Unable to get view config" errors. Always place this code in an.android.tsxcomponent file or guard it withPlatform.OS === 'android'.Hostmust be imported from@expo/ui(the universal root), not from@expo/ui/jetpack-compose.
Use this layer only when the universal @expo/ui components don't cover what you need on Android (see ./universal.md first). This requires a platform-specific tree.
File placement with Expo Router
Do not put .android.tsx files inside app/ or src/app/. Expo Router does not support platform-extension suffixes for route files and will throw a "no fallback sibling" Render Error.
Place platform-specific component files in components/ (or any directory outside the route tree), then import them from a regular route file:
src/app/product-list.tsx:
Alternatively, keep everything in one regular route file and branch on Platform.OS:
Instructions
- Expo UI's API mirrors Jetpack Compose's API. Use Jetpack Compose and Material Design 3 knowledge to decide which components or modifiers to use. If you need deeper Jetpack Compose or Material 3 guidance (e.g. which component to pick, layout patterns, theming), spawn a subagent to research Jetpack Compose and Material Design 3 best practices.
- Components are imported from
@expo/ui/jetpack-compose, modifiers from@expo/ui/jetpack-compose/modifiers. - Before writing any code, run the list-components script to get the exact components and modifiers available in the installed version:
(
<skill-root>is the directory containing thisreferences/folder.) - Always read the
.d.tstype files to confirm prop shapes and signatures — read the relevant{ComponentName}/index.d.tsfrom the installed@expo/ui/jetpack-composepackage innode_modules. This is the most reliable source of truth. - When about to use a component, fetch its docs to confirm the API — https://docs.expo.dev/versions/latest/sdk/ui/jetpack-compose/{component-name}/index.md
- When unsure about a modifier's API, refer to the docs — https://docs.expo.dev/versions/latest/sdk/ui/jetpack-compose/modifiers/index.md
- Every Jetpack Compose tree must be wrapped in
Host. Use<Host matchContents>for intrinsic sizing, or<Host style={{ flex: 1 }}>when you need explicit size (e.g. as a parent ofLazyColumn). Example:
RNHostViewembeds React Native components inside a Jetpack Compose tree (the same concept as in@expo/ui/swift-ui) — wrap any RN child in<RNHostView>.- If a required composable or modifier is missing in Expo UI, it can be extended via a local Expo module. See: https://docs.expo.dev/guides/expo-ui-jetpack-compose/extending/index.md. Confirm with the user before extending.
Key Components
- LazyColumn — Use instead of react-native
ScrollView/FlatListfor scrollable lists. Wrap in<Host style={{ flex: 1 }}>. Not suitable for large lists — each item is a JSX node processed on the JS thread, which causes noticeable slowdowns at scale. - Icon — Use
<Icon source={require('./icon.xml')} size={24} />with Android XML vector drawables. To get icons: go to Material Symbols, select an icon, choose the Android platform, and download the XML vector drawable. Save these as.xmlfiles in your project'sassets/directory (e.g.assets/icons/wifi.xml). Metro bundles.xmlassets automatically — no metro config changes needed.
useNativeState
useNativeState creates observable state that updates synchronously on the UI thread via worklets, enabling immediate native state changes without waiting for a React render cycle. Requires react-native-worklets — without it updates still go through React and flickering remains. Best for real-time interactions where synchronous updates matter, e.g. a text field that masks or formats input as the user types.
ObservableState.valueis readable/writable from worklets;onChangefires a worklet listener on state change.- Docs — https://docs.expo.dev/versions/latest/sdk/ui/jetpack-compose/usenativestate/index.md