expo-ui
Build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android rendered from React in an Expo or React Native app. Covers universal cross-platform components (Host, Column, Row, Button, Text, List, and more imported from @expo/ui), drop-in replaceme
Install
npx skills add https://github.com/sickn33/agentic-awesome-skills/tree/main/skills/expo-ui
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install sickn33-agentic-awesome-skills@llmmart
git clone https://github.com/sickn33/agentic-awesome-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole sickn33/agentic-awesome-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Expo UI (@expo/ui)
When to Use
Use this skill when you need build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android rendered from React in an Expo or React Native app. Covers universal cross-platform components (Host, Column, Row, Button, Text, List, and more imported from @expo/ui), drop-in replacements...
@expo/ui renders real native UI from React: SwiftUI on iOS, Jetpack Compose on Android. Start with its universal components (one tree for iOS, Android, and web) and drop to platform-specific SwiftUI/Jetpack Compose only when the universal layer falls short. It also ships drop-in replacements for migrating off RN community UI libraries.
These instructions track the latest Expo SDK. The universal layer requires SDK 56+. Drop-in replacements and the platform-specific layers also exist on SDK 55. For component details on a specific SDK, refer to the Expo UI docs for that version.
Installation
npx expo install @expo/ui
On SDK 56, @expo/ui works in Expo Go, so npx expo start runs it directly — no custom build required. On older SDKs, build a dev client first (npx expo run:ios / npx expo run:android).
Every @expo/ui tree — universal or platform-specific — must be wrapped in Host.
Choosing an approach (read this first)
Work down this list and stop at the first layer that meets the need:
Universal components — start here. Import from the
@expo/uiroot. One component tree runs unmodified on iOS, Android, and web from a single source (Compose on Android, SwiftUI on iOS,react-native-web/react-domon web). No platform file splits. →./references/universal.mdPlatform-specific (SwiftUI / Jetpack Compose). Import from
@expo/ui/swift-uior@expo/ui/jetpack-compose. Use only when the universal layer is missing a component or modifier you need, or when you need platform-specific behavior or optimization. Downside: you write two trees and split them into.ios.tsx/.android.tsxfiles (or branch onPlatform.OS) — more code to maintain.@expo/ui/swift-uiis iOS-only.@expo/ui/jetpack-composeis Android-only. Importing either in a file that runs on the other platform will crash at runtime with "Unable to get view config" errors. Isolate platform-specific trees in.ios.tsx/.android.tsxfiles placed incomponents/(never insideapp/— Expo Router does not support platform extensions for route files), or guard withPlatform.OSin a regular route file.Hostmust always be imported from@expo/ui(the universal package root), not from the platform-specific sub-packages. →./references/swift-ui.mdand./references/jetpack-compose.md
Already using an RN community UI library? @expo/ui also ships drop-in replacements — API-compatible swaps for popular libraries (@gorhom/bottom-sheet, @react-native-community/datetimepicker, and more), imported from @expo/ui/community/<name>. This is a migration side-path for replacing an existing dependency, not a step in the universal-vs-platform decision above. → ./references/drop-in-replacements.md
References
Consult these resources as needed:
references/
universal.md Universal @expo/ui components and when to use them (SDK 56+)
drop-in-replacements.md API-compatible replacements for RN community UI libraries
swift-ui.md Platform-specific iOS UI: @expo/ui/swift-ui components, modifiers, RNHostView, useNativeState
jetpack-compose.md Platform-specific Android UI: @expo/ui/jetpack-compose components, modifiers, LazyColumn caveat, icons, useNativeState
Limitations
- Use this skill only when the task clearly matches its upstream product or API scope.
- Verify commands, API behavior, pricing, quotas, credentials, and deployment effects against current official documentation before making changes.
- Do not treat generated examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.
Files (agentic-awesome-skills)
-
agents
-
openai.yaml 722 B
interface: display_name: "Expo UI" short_description: "Build native UI with @expo/ui — universal components first, SwiftUI/Jetpack Compose when needed" default_prompt: "Use $expo-ui when adding or reviewing @expo/ui UI: universal cross-platform components (Host, Column, Row, Button, Text, List, ...), drop-in replacements for React Native community libraries, or platform-specific @expo/ui/swift-ui and @expo/ui/jetpack-compose trees and modifiers. Recommend universal components first; use drop-in replacements when migrating off a community UI library; reach for platform-specific layers only when universal is missing something or you need platform optimization (which requires .ios.tsx/.android.tsx splits)."
-
-
references
-
drop-in-replacements.md 2 KB
# Drop-in replacements for RN community libraries `@expo/ui` ships API-compatible replacements for popular React Native community libraries, powered by native `@expo/ui` components (Jetpack Compose on Android, SwiftUI on iOS). Use these when migrating an existing app off a community UI dependency — the API matches the library being replaced, so the swap is usually just the import path. ## Available replacements Every drop-in lives under `@expo/ui/community/<kebab-case-name>`. Note which are default vs named imports. | Replaces | Import | |----------|--------| | `@gorhom/bottom-sheet` | `import BottomSheet, { BottomSheetView } from '@expo/ui/community/bottom-sheet'` | | `@react-native-community/datetimepicker` | `import DateTimePicker from '@expo/ui/community/datetime-picker'` | | `@react-native-masked-view/masked-view` | `import { MaskedView } from '@expo/ui/community/masked-view'` | | `@react-native-menu/menu` | `import { MenuView } from '@expo/ui/community/menu'` | | `react-native-pager-view` | `import PagerView from '@expo/ui/community/pager-view'` | | `@react-native-picker/picker` | `import { Picker } from '@expo/ui/community/picker'` | | `@react-native-segmented-control/segmented-control` | `import SegmentedControl from '@expo/ui/community/segmented-control'` | | `@react-native-community/slider` | `import Slider from '@expo/ui/community/slider'` | ## Confirming the API Each component has a dedicated docs page with setup and usage: - Overview — https://docs.expo.dev/versions/latest/sdk/ui/drop-in-replacements/index.md - Per component — https://docs.expo.dev/versions/latest/sdk/ui/drop-in-replacements/{component}/index.md (slug is the component name lowercased, no hyphens, e.g. `bottomsheet`, `datetimepicker`, `segmentedcontrol`) The installed package's TypeScript types (`.d.ts`) are the most reliable source of truth for the exact props on your SDK version (@expo/ui is versioned with the SDK and its API can change between versions). Use the doc page to find platform support and any props that differ from the library being replaced. -
jetpack-compose.md 5.4 KB
# Platform-specific Android UI: `@expo/ui/jetpack-compose` > **Android only.** Code that imports from `@expo/ui/jetpack-compose` will crash on iOS with "Unable to get view config" errors. Always place this code in an `.android.tsx` component file or guard it with `Platform.OS === 'android'`. `Host` must 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/components/ProductList.android.tsx ← Compose tree lives here src/app/product-list.tsx ← regular Expo Router route, imports the component ``` `src/app/product-list.tsx`: ```tsx import ProductList from '../components/ProductList'; export default ProductList; ``` Alternatively, keep everything in one regular route file and branch on `Platform.OS`: ```tsx // src/app/product-list.tsx import { Platform } from 'react-native'; const ComposeList = Platform.OS === 'android' ? require('../components/ProductList.android').default : null; ``` ## 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](https://developer.android.com/develop/ui/compose/components) and [Material Design 3](https://m3.material.io/) 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: ```bash node <skill-root>/scripts/list-components.js <project-path> # names only (compact) node <skill-root>/scripts/list-components.js <project-path> --docs # with one-line descriptions ``` (`<skill-root>` is the directory containing this `references/` folder.) - **Always read the `.d.ts` type files** to confirm prop shapes and signatures — read the relevant `{ComponentName}/index.d.ts` from the installed `@expo/ui/jetpack-compose` package in `node_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 of `LazyColumn`). Example: ```jsx import { Host } from "@expo/ui"; // Host always from universal root import { Column, Button, Text } from "@expo/ui/jetpack-compose"; import { fillMaxWidth, paddingAll } from "@expo/ui/jetpack-compose/modifiers"; <Host matchContents> <Column verticalArrangement={{ spacedBy: 8 }} modifiers={[fillMaxWidth(), paddingAll(16)]}> <Text style={{ typography: "titleLarge" }}>Hello</Text> <Button onPress={() => alert("Pressed!")}>Press me</Button> </Column> </Host>; ``` - `RNHostView` embeds 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`/`FlatList` for 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](https://fonts.google.com/icons), select an icon, choose the Android platform, and download the XML vector drawable. Save these as `.xml` files in your project's `assets/` directory (e.g. `assets/icons/wifi.xml`). Metro bundles `.xml` assets 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.value` is readable/writable from worklets; `onChange` fires a worklet listener on state change. - Docs — https://docs.expo.dev/versions/latest/sdk/ui/jetpack-compose/usenativestate/index.md -
swift-ui.md 4.3 KB
# Platform-specific iOS UI: `@expo/ui/swift-ui` > **iOS only.** Code that imports from `@expo/ui/swift-ui` will crash on Android with "Unable to get view config" errors. Always place this code in an `.ios.tsx` component file or guard it with `Platform.OS === 'ios'`. `Host` must be imported from `@expo/ui` (the universal root), not from `@expo/ui/swift-ui`. Use this layer only when the universal `@expo/ui` components don't cover what you need on iOS (see `./universal.md` first). This requires a platform-specific tree. ### File placement with Expo Router **Do not put `.ios.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/components/ProfileEditor.ios.tsx ← SwiftUI tree lives here src/app/profile-editor.tsx ← regular Expo Router route, imports the component ``` `src/app/profile-editor.tsx`: ```tsx import ProfileEditor from '../components/ProfileEditor'; export default ProfileEditor; ``` Alternatively, keep everything in one regular route file and branch on `Platform.OS`: ```tsx // src/app/profile-editor.tsx import { Platform } from 'react-native'; // import SwiftUI components only when on iOS to avoid Android crash const SwiftUIForm = Platform.OS === 'ios' ? require('../components/ProfileEditor.ios').default : null; ``` Or more simply, put the `Platform.OS` guard and the SwiftUI import in the same route file (safe because Metro only bundles `.ios.tsx` imports on iOS builds when using platform extensions in `components/`). ## Instructions - Expo UI's API mirrors SwiftUI's API. Use SwiftUI knowledge to decide which components or modifiers to use. - Components are imported from `@expo/ui/swift-ui`, modifiers from `@expo/ui/swift-ui/modifiers`. - **Before writing any code, run the list-components script** to get the exact components and modifiers available in the installed version: ```bash node <skill-root>/scripts/list-components.js <project-path> # names only (compact) node <skill-root>/scripts/list-components.js <project-path> --docs # with one-line descriptions ``` (`<skill-root>` is the directory containing this `references/` folder.) - **The installed package's TypeScript types (`.d.ts`) are the most reliable source of truth** for prop shapes and signatures — read the relevant `{Component}/index.d.ts` from the installed `@expo/ui/swift-ui` package in `node_modules`. Use the docs below as the human-readable reference. - When about to use a component, fetch its docs to confirm the API — https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/{component-name}/index.md - When unsure about a modifier's API, refer to the docs — https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/modifiers/index.md - Every SwiftUI tree must be wrapped in `Host`. - `RNHostView` is specifically for embedding RN components inside a SwiftUI tree. Example: ```jsx import { Host } from "@expo/ui"; // Host always from universal root import { VStack, RNHostView } from "@expo/ui/swift-ui"; // platform components from swift-ui import { Pressable } from "react-native"; <Host matchContents> <VStack> <RNHostView matchContents> // Here, `Pressable` is an RN component so it is wrapped in `RNHostView`. <Pressable /> </RNHostView> </VStack> </Host>; ``` - If a required modifier or View is missing in Expo UI, it can be extended via a local Expo module. See: https://docs.expo.dev/guides/expo-ui-swift-ui/extending/index.md. Confirm with the user before extending. ## 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.value` is readable/writable from worklets; `onChange` fires a worklet listener on state change. - Docs — https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/usenativestate/index.md -
universal.md 3.4 KB
# Universal `@expo/ui` components > Requires Expo SDK 56+. Universal components are a single-API layer over the platform-native UI toolkits: Jetpack Compose on Android, SwiftUI on iOS, and `react-native-web` / `react-dom` on web. You write one component tree that runs unmodified on all three platforms while keeping a native look and feel — no `.ios.tsx` / `.android.tsx` split. ## Usage Import everything, including `Host`, from the package root (`@expo/ui`). Every tree must be wrapped in `Host`. ```tsx import { Host, Column, Button, Text } from '@expo/ui'; <Host matchContents> <Column> <Text>Hello</Text> <Button onPress={() => alert('Pressed!')}>Press me</Button> </Column> </Host>; ``` ## Components | Category | Components | |----------|------------| | Container | `Host` (required root wrapper) | | Layout | `Column`, `Row`, `Spacer`, `ScrollView` | | Display | `Text`, `Icon` | | Controls | `Button`, `Switch`, `Checkbox`, `Slider`, `TextInput`, `Picker` | | Disclosure & presentation | `BottomSheet`, `Collapsible` | | Collections & forms | `List` (with `ListItem`), `FieldGroup` | > **`List` is not suitable for large lists.** Each `ListItem` is a JSX node processed on the JS thread — for large datasets this causes noticeable slowdowns. ## TextInput and useNativeState `TextInput` from `@expo/ui` is **not like React Native's TextInput** — its `value` and `selection` props take an `ObservableState` object (from `useNativeState`), not a plain string. This is what enables synchronous, flicker-free updates: when the user types, `onChangeText` runs as a worklet on the UI thread and writes directly to `value` without a React render cycle. Requires `react-native-worklets`. Without it the worklet directive has no effect and flickering remains. ```tsx import { Host, TextInput, useNativeState } from '@expo/ui'; import { useCallback } from 'react'; export default function MyInput() { const text = useNativeState(''); const handleChangeText = useCallback((value: string) => { 'worklet'; // transform synchronously on the UI thread — no React re-render text.value = value === 'Hello' ? 'World' : value; }, [text]); return ( <Host matchContents> <TextInput value={text} onChangeText={handleChangeText} placeholder="Type here" /> </Host> ); } ``` Docs — https://docs.expo.dev/versions/latest/sdk/ui/universal/textinput/index.md ## Confirming the API `@expo/ui` is versioned with the Expo SDK (e.g. `56.0.x` for SDK 56) and its API can change between SDK versions, so the **installed package's TypeScript types (`.d.ts`) are the most reliable source of truth** — they match the version in your project, while the docs track latest. Read the relevant component's `.d.ts` from the installed `@expo/ui` package in `node_modules`. Use the docs as the human-readable reference: - Overview — https://docs.expo.dev/versions/latest/sdk/ui/universal/index.md - Per component — https://docs.expo.dev/versions/latest/sdk/ui/universal/{component-name}/index.md ## When to drop down to a platform-specific layer Choose universal components whenever they cover the requirement. Drop down to `@expo/ui/swift-ui` or `@expo/ui/jetpack-compose` only when the universal API doesn't expose the component, modifier, or platform-specific behavior you need — accepting the per-platform file split that requires. See `./swift-ui.md` and `./jetpack-compose.md`.
-
-
scripts
-
list-components.js 7.4 KB
#!/usr/bin/env node /** * List available @expo/ui components and modifiers installed in a project. * * Usage: * node list-components.js <project-path> [--docs] * * --docs Include a one-line JSDoc description per modifier. * Omit (default) for a compact names-only list that consumes fewer tokens. * * Output goes to stdout. Redirect or capture it to inject into an agent prompt. */ 'use strict'; const fs = require('fs'); const path = require('path'); const sanitizeFilename = require('sanitize-filename'); const rawProjectPath = process.argv[2]; const projectPath = rawProjectPath ? path.resolve(process.cwd(), sanitizeFilename(path.basename(rawProjectPath))) : null; const withDocs = process.argv.includes('--docs'); if (!projectPath) { console.error('Usage: node list-components.js <project-path> [--docs]'); process.exit(1); } const pkgRoot = path.join(projectPath, 'node_modules', '@expo', 'ui'); if (!fs.existsSync(pkgRoot)) { console.error(`@expo/ui not found in ${projectPath}/node_modules`); process.exit(1); } // Read installed version from package.json let version = 'unknown'; try { const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8')); version = pkg.version || 'unknown'; } catch (_) {} // --------------------------------------------------------------------------- // Component extraction — parse `export * from './Name'` in an index.d.ts // --------------------------------------------------------------------------- // TypeScript type names that are not components — skip anything matching these suffixes const TYPE_SUFFIX = /(?:Props|Ref|Handle|Params|Config|Options|Type|Types|Value|Values|Colors|Style|Styles|Event|Events|Alignment|Animation|Spec)$/; function extractComponents(indexFile) { if (!fs.existsSync(indexFile)) return []; const src = fs.readFileSync(indexFile, 'utf8'); const names = []; for (const line of src.split('\n')) { // export * from './ComponentName' or export * from './ComponentName/index' const m = line.match(/^export \* from ['"]\.\/([^/'"]+)/); if (m) { const name = m[1]; // Skip non-component re-exports (types, utils, state internals) if (/^(types|utils|index|State|hooks|colors|layout-types|MaterialSymbols)/.test(name)) continue; if (TYPE_SUFFIX.test(name)) continue; names.push(name); } // export { Name, ... } from './Something' — pick up named re-exports too const n = line.match(/^export \{([^}]+)\}/); if (n) { for (const part of n[1].split(',')) { // Skip `type Foo` re-exports if (/^\s*type\s/.test(part)) continue; const id = part.trim().split(/\s+as\s+/)[0].trim(); if (id && /^[A-Z]/.test(id) && !TYPE_SUFFIX.test(id)) names.push(id); } } } return [...new Set(names)].sort(); } // --------------------------------------------------------------------------- // Modifier extraction — parse `export declare const/function <name>` in // a flat modifiers/index.d.ts, optionally with the preceding JSDoc summary. // --------------------------------------------------------------------------- function extractModifiers(modifiersFile) { if (!fs.existsSync(modifiersFile)) return []; const src = fs.readFileSync(modifiersFile, 'utf8'); const lines = src.split('\n'); const results = []; const seen = new Set(); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const m = line.match(/^export declare (?:const|function) ([a-zA-Z_][a-zA-Z0-9_]*)/); if (!m) continue; const name = m[1]; // Skip type-only helpers and internal symbols if (/^(is|filter|create|type|export)/.test(name) && name !== 'frame') continue; if (seen.has(name)) continue; seen.add(name); if (!withDocs) { results.push({ name }); continue; } // Find /** that opens the JSDoc block immediately preceding this export, // then scan forward through the block for the first plain-prose summary. let jsdocStart = -1; for (let j = i - 1; j >= 0; j--) { const jl = lines[j].trim(); if (jl === '/**') { jsdocStart = j; break; } if (jl !== '' && jl !== '*/' && !jl.startsWith('*')) break; } let summary = ''; let deprecated = false; if (jsdocStart >= 0) { let inCodeBlock = false; for (let k = jsdocStart + 1; k < i; k++) { const kl = lines[k].trim(); if (kl.startsWith('* ```')) { inCodeBlock = !inCodeBlock; continue; } if (inCodeBlock) continue; // Check @deprecated anywhere in the block (may appear after @param) if (kl.startsWith('* @deprecated')) { deprecated = true; continue; } // Extract summary from opening prose only — stop at first non-deprecated tag if (!summary) { if (kl.startsWith('* @')) continue; // skip tags while looking for prose if (kl === '*/' || kl === '/**' || kl === '*') continue; if (kl.startsWith('* ')) { const text = kl.slice(2).trim(); if (!text.startsWith('-') && !text.startsWith('<')) { summary = text.replace(/\.$/, ''); } } } } } results.push({ name, summary, deprecated }); } return results; } // --------------------------------------------------------------------------- // Format helpers // --------------------------------------------------------------------------- function formatNames(names) { return names.join(', '); } function formatModifiers(mods) { if (!withDocs) return mods.map(m => m.name).join(', '); const lines = []; for (const m of mods) { const dep = m.deprecated ? ' [deprecated]' : ''; const desc = m.summary ? ` — ${m.summary}` : ''; lines.push(` ${m.name}${dep}${desc}`); } return lines.join('\n'); } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- const buildRoot = path.join(pkgRoot, 'build'); // Universal const universalComponents = extractComponents(path.join(buildRoot, 'universal', 'index.d.ts')); // Swift-UI (iOS only) const swiftuiComponents = extractComponents(path.join(buildRoot, 'swift-ui', 'index.d.ts')); const swiftuiModifiers = extractModifiers(path.join(buildRoot, 'swift-ui', 'modifiers', 'index.d.ts')); // Jetpack Compose (Android only) const composeComponents = extractComponents(path.join(buildRoot, 'jetpack-compose', 'index.d.ts')); const composeModifiers = extractModifiers(path.join(buildRoot, 'jetpack-compose', 'modifiers', 'index.d.ts')); const docsNote = withDocs ? ' (with descriptions)' : ' (names only — run with --docs for descriptions)'; console.log(`@expo/ui ${version}${docsNote}\n`); console.log(`@expo/ui — universal (iOS + Android + web)`); console.log(` Components: ${formatNames(universalComponents)}\n`); console.log(`@expo/ui/swift-ui — iOS ONLY (crashes on Android)`); console.log(` Components: ${formatNames(swiftuiComponents)}`); if (withDocs) { console.log(` Modifiers:\n${formatModifiers(swiftuiModifiers)}`); } else { console.log(` Modifiers: ${formatModifiers(swiftuiModifiers)}`); } console.log(); console.log(`@expo/ui/jetpack-compose — Android ONLY (crashes on iOS)`); console.log(` Components: ${formatNames(composeComponents)}`); if (withDocs) { console.log(` Modifiers:\n${formatModifiers(composeModifiers)}`); } else { console.log(` Modifiers: ${formatModifiers(composeModifiers)}`); }
-
-
SKILL.md 4.5 KB
--- name: expo-ui description: "Build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android rendered from React in an Expo or React Native app. Covers universal cross-platform components (Host, Column, Row, Button, Text, List, and more imported from @expo/ui), drop-in replacements..." risk: critical source: https://github.com/expo/skills/tree/main/plugins/expo/skills/expo-ui source_repo: expo/skills source_type: official date_added: 2026-07-01 license: MIT license_source: https://github.com/expo/skills/blob/main/LICENSE --- # Expo UI (`@expo/ui`) ## When to Use Use this skill when you need build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android rendered from React in an Expo or React Native app. Covers universal cross-platform components (Host, Column, Row, Button, Text, List, and more imported from @expo/ui), drop-in replacements... `@expo/ui` renders real native UI from React: SwiftUI on iOS, Jetpack Compose on Android. Start with its universal components (one tree for iOS, Android, and web) and drop to platform-specific SwiftUI/Jetpack Compose only when the universal layer falls short. It also ships drop-in replacements for migrating off RN community UI libraries. > These instructions track the latest Expo SDK. The **universal** layer requires **SDK 56+**. Drop-in replacements and the platform-specific layers also exist on SDK 55. For component details on a specific SDK, refer to the Expo UI docs for that version. ## Installation ```bash npx expo install @expo/ui ``` On SDK 56, `@expo/ui` works in Expo Go, so `npx expo start` runs it directly — no custom build required. On older SDKs, build a dev client first (`npx expo run:ios` / `npx expo run:android`). Every `@expo/ui` tree — universal or platform-specific — must be wrapped in `Host`. ## Choosing an approach (read this first) Work down this list and stop at the first layer that meets the need: 1. **Universal components — start here.** Import from the `@expo/ui` root. One component tree runs unmodified on iOS, Android, and web from a single source (Compose on Android, SwiftUI on iOS, `react-native-web`/`react-dom` on web). No platform file splits. → `./references/universal.md` 2. **Platform-specific (SwiftUI / Jetpack Compose).** Import from `@expo/ui/swift-ui` or `@expo/ui/jetpack-compose`. Use **only** when the universal layer is missing a component or modifier you need, or when you need platform-specific behavior or optimization. **Downside:** you write two trees and split them into `.ios.tsx` / `.android.tsx` files (or branch on `Platform.OS`) — more code to maintain. > **`@expo/ui/swift-ui` is iOS-only. `@expo/ui/jetpack-compose` is Android-only.** Importing either in a file that runs on the other platform will crash at runtime with "Unable to get view config" errors. Isolate platform-specific trees in `.ios.tsx` / `.android.tsx` files placed in `components/` (never inside `app/` — Expo Router does not support platform extensions for route files), or guard with `Platform.OS` in a regular route file. `Host` must always be imported from `@expo/ui` (the universal package root), not from the platform-specific sub-packages. → `./references/swift-ui.md` and `./references/jetpack-compose.md` **Already using an RN community UI library?** `@expo/ui` also ships **drop-in replacements** — API-compatible swaps for popular libraries (`@gorhom/bottom-sheet`, `@react-native-community/datetimepicker`, and more), imported from `@expo/ui/community/<name>`. This is a migration side-path for replacing an existing dependency, not a step in the universal-vs-platform decision above. → `./references/drop-in-replacements.md` ## References Consult these resources as needed: ``` references/ universal.md Universal @expo/ui components and when to use them (SDK 56+) drop-in-replacements.md API-compatible replacements for RN community UI libraries swift-ui.md Platform-specific iOS UI: @expo/ui/swift-ui components, modifiers, RNHostView, useNativeState jetpack-compose.md Platform-specific Android UI: @expo/ui/jetpack-compose components, modifiers, LazyColumn caveat, icons, useNativeState ``` ## Limitations - Use this skill only when the task clearly matches its upstream product or API scope. - Verify commands, API behavior, pricing, quotas, credentials, and deployment effects against current official documentation before making changes. - Do not treat generated examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.