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/universal.md

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.

import { Host, Column, Button, Text } from '@expo/ui';

<Host matchContents>
  <Column>
    <Text>Hello</Text>
    <Button onPress={() => alert('Pressed!')}>Press me</Button>
  </Column>
</Host>;

Components

CategoryComponents
ContainerHost (required root wrapper)
LayoutColumn, Row, Spacer, ScrollView
DisplayText, Icon
ControlsButton, Switch, Checkbox, Slider, TextInput, Picker
Disclosure & presentationBottomSheet, Collapsible
Collections & formsList (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.

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

BottomSheet

BottomSheet is a universal slide-up sheet. Control it with isPresented / onDismiss — not isOpen, isOpened, onIsOpenedChange, or onChange (those belong to other sheet APIs; the universal BottomSheet does not accept them). snapPoints is optional and accepts 'half', 'full', { fraction }, or { height }; when omitted the sheet auto-sizes to its content.

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

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

  return (
    <Host>
      <Button onPress={() => setIsOpen(true)}>Show details</Button>
      <BottomSheet
        isPresented={isOpen}
        onDismiss={() => setIsOpen(false)}
        snapPoints={['half', 'full']}
      >
        <Column>
          <Text>Sheet content</Text>
          <Button onPress={() => setIsOpen(false)}>Close</Button>
        </Column>
      </BottomSheet>
    </Host>
  );
}

Don't confuse the universal BottomSheet with the drop-in @gorhom/bottom-sheet replacement (@expo/ui/community/bottom-sheet). They are different components with different APIs. Use the universal one when building new UI; use the drop-in only when migrating an existing @gorhom/bottom-sheet integration. See ./drop-in-replacements.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:

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.

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