Expo UI Full Reference (@expo/ui)
The complete @expo/ui reference: every layer (universal, SwiftUI, Jetpack Compose), the full
modifier catalogs, drop-in replacements, screen templates, and tooling. Read the mental model
below, then load the one reference file the task needs (map at the bottom).
@expo/ui renders real native UI from React: SwiftUI views on iOS, Jetpack Compose on
Android. It is a primitives library (like <div>/<span> for the OS), not a styled design
kit. Reach for it when you want controls that look and behave exactly like the system: settings
forms, pickers, sheets, menus, toggles, sliders. Keep plain React Native View/Text
(NativeWind, flexbox) for custom-branded, heavily-styled UI. The two mix freely at the
component level.
Mental model (the one thing to get right)
Every @expo/ui tree is an island of native UI bridged into your RN tree by a Host.
Think of Host like <svg> in the DOM or <Canvas> in Skia: the bridge boundary. This drives
the #1 rule agents get wrong:
Flexbox / Yoga styles apply to the Host only. Inside a Host, there is no Yoga.
Lay out children with native primitives and their props/modifiers, never with RN
style={{ flex, flexDirection, justifyContent, alignItems, gap }}:
- Universal/SwiftUI:
Row/Column/HStack/VStack with spacing + alignment.
- Compose:
Row/Column with horizontalArrangement/verticalArrangement +
modifiers={[...]} from @expo/ui/jetpack-compose/modifiers.
Putting style={{ flex: 1 }} on a Button/Text/Row inside a Host is a no-op or a bug.
scripts/audit-expo-ui.js catches this automatically.
Host is always imported from the @expo/ui root, even when its children are
platform-specific. Host itself takes either matchContents (size to content, best for inline
widgets) or style={{ flex: 1 }} / explicit size (required when it wraps a scrollable like
List, ScrollView, LazyColumn).
Choosing a layer (stop at the first that fits)
- Universal first: import from
@expo/ui. One tree runs unmodified on iOS, Android, and
web (web is experimental). No .ios.tsx/.android.tsx split. Covers the common kit: layout,
Text, Icon, Button, Switch, Checkbox, Slider, TextInput, Picker,
BottomSheet, Collapsible, List, FieldGroup. → references/universal.md
- Platform-specific: import from
@expo/ui/swift-ui (iOS) or @expo/ui/jetpack-compose
(Android). Use only when universal lacks the component, modifier, or platform behavior you
need (e.g. SwiftUI Form/ContextMenu/glassEffect, Compose
DockedSearchBar/FloatingActionButton/Material 3). Cost: two trees and a file split.
→ references/swift-ui.md, references/jetpack-compose.md
- Drop-in replacements:
@expo/ui/community/<name>. A migration side-path for swapping
out an RN community UI dependency (@gorhom/bottom-sheet,
@react-native-community/datetimepicker, ...), not part of the decision above.
→ references/drop-in-replacements.md
Rules agents get wrong (read before writing code)
- Platform packages crash on the wrong OS. Importing
@expo/ui/swift-ui on Android or
@expo/ui/jetpack-compose on iOS throws Unable to get view config. Isolate platform trees
in .ios.tsx / .android.tsx files under components/ (never inside app/, where Expo
Router rejects platform suffixes on route files with a "no fallback sibling" error), or guard
with Platform.OS. Re-export from a plain route file.
- The callback prop name differs by layer. Universal
Button and SwiftUI Button use
onPress; Compose Button uses onClick. Don't assume.
TextInput/TextField value is not a plain string when using useNativeState: it takes
an ObservableState for flicker-free worklet updates, and needs react-native-worklets. See
the universal/platform references.
List / LazyColumn items run on the JS thread (each row is a JSX node): fine for
settings-sized lists, not for large data sets.
- Modifiers mirror native APIs. Apple/Google sample code is roughly a search-and-replace
away. Apply them via the
modifiers={[...]} prop (order matters), importing from
@expo/ui/swift-ui/modifiers or @expo/ui/jetpack-compose/modifiers.
SDK gating (confirm against the installed version)
- Universal layer needs SDK 56+. Platform-specific (
swift-ui, jetpack-compose) and
drop-ins also work on SDK 55.
- On SDK 56+,
@expo/ui ships in Expo Go, so npx expo start runs it directly. On SDK
55, build a dev client (npx expo run:ios / run:android).
Source of truth: this skill is a fast map, not the territory
@expo/ui is versioned with the Expo SDK and its props/modifiers shift between cycles, so the
tables here are a fast map keyed to SDK 56, never the authority. When the map is ambiguous,
looks stale, conflicts with what is installed, or is missing a component / prop / modifier you
need, stop guessing and confirm against the real source, in this order of authority:
- The installed package (authoritative for the project's version). Prop shapes live in
node_modules/@expo/ui/build/<layer>/<Component>/index.d.ts. Enumerate what is installed:node <skill-root>/scripts/list-components.js <project-path> # component + modifier names
node <skill-root>/scripts/list-components.js <project-path> --docs # + one-line modifier descriptions
- The docs, matched to the project's SDK. Expo serves agent-friendly Markdown: append
/index.md to any page, e.g.
https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/button/index.md. versions/latest
tracks the newest SDK (57 as of writing), so if the project is on 56 use
https://docs.expo.dev/versions/v56.0.0/... to match. Full page index:
https://docs.expo.dev/llms.txt; UI overview: .../sdk/ui/index.md.
Installed types win over the docs; the docs win over this map. Never invent a prop or modifier
to fill a gap: confirm it exists first.
Reference map (load only the one you need)
references/
universal.md '@expo/ui' cross-platform components, START HERE (SDK 56+)
swift-ui.md '@expo/ui/swift-ui' iOS components (SDK 55+, iOS only)
swift-ui-modifiers.md full SwiftUI modifier catalog (~95), load for any non-obvious modifier
jetpack-compose.md '@expo/ui/jetpack-compose' Android components (SDK 55+, Android only)
jetpack-compose-modifiers.md full Compose modifier catalog
drop-in-replacements.md '@expo/ui/community/*' API-compatible swaps (migration side-path)
assets/
universal-screen.tsx copy-paste universal settings screen (Expo Router)
swiftui-screen.ios.tsx copy-paste SwiftUI-only screen
compose-screen.android.tsx copy-paste Compose-only screen
install.md install + config snippet
scripts/
list-components.js enumerate components/modifiers in the *installed* version
audit-expo-ui.js flag flexbox-on-@expo/ui and avoidable platform imports
A task touching one platform pulls in only that one reference file. Shared concepts (Host,
the flexbox rule, the layer decision, SDK gating, source of truth) live here and are not
repeated in the references.
1---2name: expo-ui-full-reference3description: Build native UI from React with @expo/ui: real SwiftUI on iOS and Jetpack Compose on Android, in an Expo or React Native + TypeScript app. Use whenever building or reviewing native-feeling UI that plain React Native handles poorly: a settings screen, grouped form, native picker, context menu, action/bottom sheet, segmented control, toggle, slider, or any Host/RNHostView tree. Also when choosing universal ('@expo/ui') vs platform-specific ('@expo/ui/swift-ui', '@expo/ui/jetpack-compose') components, applying SwiftUI/Compose modifiers, swapping an RN community library for an '@expo/ui/community/*' drop-in, or fixing 'Unable to get view config' crashes. Not for custom native modules, Expo Router navigation, Reanimated, or data fetching.4license: MIT5---67# Expo UI Full Reference (`@expo/ui`)89The complete `@expo/ui` reference: every layer (universal, SwiftUI, Jetpack Compose), the full10modifier catalogs, drop-in replacements, screen templates, and tooling. Read the mental model11below, then load the one reference file the task needs (map at the bottom).1213`@expo/ui` renders **real native UI from React**: SwiftUI views on iOS, Jetpack Compose on14Android. It is a *primitives* library (like `<div>`/`<span>` for the OS), not a styled design15kit. Reach for it when you want controls that look and behave exactly like the system: settings16forms, pickers, sheets, menus, toggles, sliders. Keep plain React Native `View`/`Text`17(NativeWind, flexbox) for custom-branded, heavily-styled UI. The two mix freely at the18component level.1920## Mental model (the one thing to get right)2122Every `@expo/ui` tree is an **island of native UI** bridged into your RN tree by a **`Host`**.23Think of `Host` like `<svg>` in the DOM or `<Canvas>` in Skia: the bridge boundary. This drives24the #1 rule agents get wrong:2526> **Flexbox / Yoga styles apply to the `Host` only. Inside a Host, there is no Yoga.**27> Lay out children with native primitives and their props/modifiers, never with RN28> `style={{ flex, flexDirection, justifyContent, alignItems, gap }}`:29> - Universal/SwiftUI: `Row`/`Column`/`HStack`/`VStack` with `spacing` + `alignment`.30> - Compose: `Row`/`Column` with `horizontalArrangement`/`verticalArrangement` +31> `modifiers={[...]}` from `@expo/ui/jetpack-compose/modifiers`.32> Putting `style={{ flex: 1 }}` on a `Button`/`Text`/`Row` inside a Host is a no-op or a bug.33> `scripts/audit-expo-ui.js` catches this automatically.3435`Host` is **always imported from the `@expo/ui` root**, even when its children are36platform-specific. `Host` itself takes either `matchContents` (size to content, best for inline37widgets) or `style={{ flex: 1 }}` / explicit size (required when it wraps a scrollable like38`List`, `ScrollView`, `LazyColumn`).3940## Choosing a layer (stop at the first that fits)41421. **Universal first**: import from `@expo/ui`. One tree runs unmodified on iOS, Android, and43 web (web is experimental). No `.ios.tsx`/`.android.tsx` split. Covers the common kit: layout,44 `Text`, `Icon`, `Button`, `Switch`, `Checkbox`, `Slider`, `TextInput`, `Picker`,45 `BottomSheet`, `Collapsible`, `List`, `FieldGroup`. → `references/universal.md`462. **Platform-specific**: import from `@expo/ui/swift-ui` (iOS) or `@expo/ui/jetpack-compose`47 (Android). Use **only** when universal lacks the component, modifier, or platform behavior you48 need (e.g. SwiftUI `Form`/`ContextMenu`/`glassEffect`, Compose49 `DockedSearchBar`/`FloatingActionButton`/Material 3). **Cost:** two trees and a file split.50 → `references/swift-ui.md`, `references/jetpack-compose.md`513. **Drop-in replacements**: `@expo/ui/community/<name>`. A *migration side-path* for swapping52 out an RN community UI dependency (`@gorhom/bottom-sheet`,53 `@react-native-community/datetimepicker`, ...), not part of the decision above.54 → `references/drop-in-replacements.md`5556## Rules agents get wrong (read before writing code)5758- **Platform packages crash on the wrong OS.** Importing `@expo/ui/swift-ui` on Android or59 `@expo/ui/jetpack-compose` on iOS throws `Unable to get view config`. Isolate platform trees60 in `.ios.tsx` / `.android.tsx` files under `components/` (never inside `app/`, where Expo61 Router rejects platform suffixes on route files with a "no fallback sibling" error), or guard62 with `Platform.OS`. Re-export from a plain route file.63- **The callback prop name differs by layer.** Universal `Button` and SwiftUI `Button` use64 `onPress`; Compose `Button` uses `onClick`. Don't assume.65- **`TextInput`/`TextField` value is not a plain string** when using `useNativeState`: it takes66 an `ObservableState` for flicker-free worklet updates, and needs `react-native-worklets`. See67 the universal/platform references.68- **`List` / `LazyColumn` items run on the JS thread** (each row is a JSX node): fine for69 settings-sized lists, not for large data sets.70- **Modifiers mirror native APIs.** Apple/Google sample code is roughly a search-and-replace71 away. Apply them via the `modifiers={[...]}` prop (order matters), importing from72 `@expo/ui/swift-ui/modifiers` or `@expo/ui/jetpack-compose/modifiers`.7374## SDK gating (confirm against the installed version)7576- **Universal layer needs SDK 56+.** Platform-specific (`swift-ui`, `jetpack-compose`) and77 drop-ins also work on **SDK 55**.78- On **SDK 56+**, `@expo/ui` ships in Expo Go, so `npx expo start` runs it directly. On **SDK79 55**, build a dev client (`npx expo run:ios` / `run:android`).8081## Source of truth: this skill is a fast map, not the territory8283`@expo/ui` is versioned with the Expo SDK and its props/modifiers shift between cycles, so the84tables here are a **fast map keyed to SDK 56**, never the authority. When the map is ambiguous,85looks stale, conflicts with what is installed, or is missing a component / prop / modifier you86need, stop guessing and confirm against the real source, in this order of authority:87881. **The installed package** (authoritative for the project's version). Prop shapes live in89 `node_modules/@expo/ui/build/<layer>/<Component>/index.d.ts`. Enumerate what is installed:90 ```bash91 node <skill-root>/scripts/list-components.js <project-path> # component + modifier names92 node <skill-root>/scripts/list-components.js <project-path> --docs # + one-line modifier descriptions93 ```942. **The docs, matched to the project's SDK.** Expo serves agent-friendly Markdown: append95 `/index.md` to any page, e.g.96 `https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/button/index.md`. `versions/latest`97 tracks the newest SDK (**57** as of writing), so if the project is on 56 use98 `https://docs.expo.dev/versions/v56.0.0/...` to match. Full page index:99 `https://docs.expo.dev/llms.txt`; UI overview: `.../sdk/ui/index.md`.100101Installed types win over the docs; the docs win over this map. Never invent a prop or modifier102to fill a gap: confirm it exists first.103104## Reference map (load only the one you need)105106```107references/108 universal.md '@expo/ui' cross-platform components, START HERE (SDK 56+)109 swift-ui.md '@expo/ui/swift-ui' iOS components (SDK 55+, iOS only)110 swift-ui-modifiers.md full SwiftUI modifier catalog (~95), load for any non-obvious modifier111 jetpack-compose.md '@expo/ui/jetpack-compose' Android components (SDK 55+, Android only)112 jetpack-compose-modifiers.md full Compose modifier catalog113 drop-in-replacements.md '@expo/ui/community/*' API-compatible swaps (migration side-path)114assets/115 universal-screen.tsx copy-paste universal settings screen (Expo Router)116 swiftui-screen.ios.tsx copy-paste SwiftUI-only screen117 compose-screen.android.tsx copy-paste Compose-only screen118 install.md install + config snippet119scripts/120 list-components.js enumerate components/modifiers in the *installed* version121 audit-expo-ui.js flag flexbox-on-@expo/ui and avoidable platform imports122```123124A task touching one platform pulls in **only that one reference file**. Shared concepts (Host,125the flexbox rule, the layer decision, SDK gating, source of truth) live here and are not126repeated in the references.