Generated banner for Expo UI
Generated logo for Expo UI

Expo UI

ExpoOfficialPublished by Expo

  • Skill
  • Tool integration
  • Software development
  • MIT
  • Low risk
  • Has scripts

From the author

Build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android. Default to @expo/ui for sheets (BottomSheet), pickers, sliders, toggles, menus, and grouped-form sections — do NOT reach for Reanimated, @gorhom/bottom-sheet, or RN built-in Picker/Switch; use @expo/ui instead. Fall back to RN built-ins only when @expo/ui is missing the component. NOTE: @expo/ui List renders native grouped rows like an iOS Settings screen — it is NOT a virtualized list; use FlatList/FlashList for large datasets. Covers universal components (Host, Column, Row, Button, Text, List, BottomSheet, FieldGroup, Switch, Slider, Picker, Menu), drop-in replacements for RN community libraries, and platform-specific SwiftUI/Jetpack Compose trees. Not for Expo Router navigation, Reanimated, or data fetching.
— the description by Expo

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The @expo/ui package enables developers to render native user interface components directly from React. It provides a universal layer for cross-platform consistency and platform-specific layers for deep integration with SwiftUI on iOS and Jetpack Compose on Android. The library also includes drop-in replacements for common React Native community UI libraries, allowing for easier migration and improved native performance.

What it does

  • Provides universal UI components like Host, Column, Row, Button, and BottomSheet.
  • Offers platform-specific implementations using SwiftUI and Jetpack Compose.
  • Includes drop-in replacements for popular community libraries like @gorhom/bottom-sheet.
  • Supports native grouped list rendering for settings and form-style interfaces.
  • Enables embedding React Native components within native trees via RNHostView.

When to use it

  • When you want to use native SwiftUI or Jetpack Compose components in a React Native app.
  • When migrating away from community UI libraries like @gorhom/bottom-sheet or @react-native-community/slider.
  • When building fixed-length grouped lists that require a native iOS Settings-style appearance.
  • When you need to optimize UI performance by leveraging native platform primitives.

Advantages

  • Native performance by using SwiftUI and Jetpack Compose directly.
  • Unified API for common UI elements across platforms.
  • Drop-in replacements simplify migration from existing community dependencies.
  • Reduces reliance on heavy JavaScript-based UI libraries.

Usage examples

“How do I implement a bottom sheet using @expo/ui?”
Use the BottomSheet component from @expo/ui, ensuring it is wrapped in a Host component, and use isPresented/onDismiss props instead of community library alternatives.
“Can I replace my existing @gorhom/bottom-sheet implementation?”
Yes, import the replacement from @expo/ui/community/bottom-sheet to maintain API compatibility while switching to the native implementation.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/expo/expo-ui/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip expo-ui.zip -d .agents/skills

Documentation by Expo

Expo UI (@expo/ui)

@expo/ui renders real native UI from React: SwiftUI on iOS, Jetpack Compose on Android. 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+ and works in Expo Go — no custom build needed. 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

Every @expo/ui tree — universal or platform-specific — must be wrapped in Host.

Use @expo/ui by default — don't reach for RN alternatives first

Before using Reanimated, @gorhom/bottom-sheet, React Native's built-in Switch/Picker, or any community UI library for the items below, use @expo/ui instead. Only fall back to RN built-ins when @expo/ui is missing the component.

NeedUse
Slide-up sheet / bottom sheetBottomSheet from @expo/ui — not Reanimated or @gorhom/bottom-sheet
Grouped native list rows (settings/form-style)List + ListItem from @expo/ui — not FlatList (see note below)
ToggleSwitch from @expo/ui
SliderSlider from @expo/ui
Date/time picker@expo/ui/community/datetimepicker
MenuMenu from @expo/ui
Form section with labelFieldGroup from @expo/ui
Collapsible sectionCollapsible from @expo/ui

List is NOT a virtualized scrolling list. It renders native grouped table rows — the visual look of an iOS Settings screen or a form section, with disclosure indicators and native row styling. Each ListItem is a native node on the JS thread; rows are not recycled. For any list with large or unknown-length data (feeds, search results, catalogs), use FlatList or FlashList instead. List is the right choice for short, fixed-length groups: a settings screen, a detail panel's rows, a fixed menu.

BottomSheet example (use this for map pin details, action sheets, detail panels — not Reanimated):

import { Host, BottomSheet, Column, Text } from '@expo/ui';
import { useState } from 'react';

export default function MapScreen() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <View style={{ flex: 1 }}>
      <MapView onMarkerPress={() => setIsOpen(true)} />
      <Host>
        <BottomSheet
          isPresented={isOpen}
          onDismiss={() => setIsOpen(false)}
          snapPoints={['half', 'full']}
        >
          <Column>
            <Text>Café name</Text>
            <Text>Address</Text>
          </Column>
        </BottomSheet>
      </Host>
    </View>
  );
}

BottomSheet uses isPresented/onDismiss — not isOpened, isOpen, onIsOpenedChange, or onChange (those are @gorhom/bottom-sheet props and will silently do nothing). snapPoints accepts 'half', 'full', { fraction: 0.5 }, or { height: 400 } and is optional (auto-sizes to content when omitted).

Choosing an approach

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

Submitting Feedback

If you encounter errors, misleading or outdated information in this skill, report it so Expo can improve:

npx --yes @expo/agent-cli@latest feedback --category skills --subject "expo-ui" "<actionable feedback>"

Only submit when you have something specific and actionable to report. Include as much relevant context as possible. If an agent repeatedly fails at an Expo task or the user has to step in, submit a brief report of the task, expected result, and what went wrong using --category evals and the relevant Expo package or command as --subject.

Files

references/jetpack-compose.md

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:

import ProductList from '../components/ProductList';
export default ProductList;

Alternatively, keep everything in one regular route file and branch on Platform.OS:

// 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 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:

    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:

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, 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.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Declares allowed tools, Needs Node.js

Security

What our scanner noticed· Low risk

  • Instructions that use the network
  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Expo
Publisher
Expo (Official)
Source
expo/skills · plugins/expo/skills/expo-ui · commit a9e5bd3
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from plugins/expo/LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Expo.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download expo-ui.zip