All skills
expo avatar

/expo-ui

@472d040 official
by expoexpo/skills2.6k stars
156

Framework (OSS). 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.

Use this Skill: https://skilld.dev/gh/expo/skills/expo-ui

This session only. Nothing lands on disk.

referencesuniversal.md

≈1.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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

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.

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.

Source: SKILL.md on GitHub

No alerts1mo3 checks · Risk SAFE
  • Gen Agent Trust Hub1mo

    This skill is safe and provides legitimate tools for building native UI with Expo. It includes a helper script for component discovery and a feedback utility from the official vendor.

  • Socket1mo

    No alerts

  • Snyk1mo

    Risk: LOW · No issues

Signed by skilld at 472d040. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated last month
What it can do
Runs commands
version
1.0.0
All 1 allowed tools
Bash(node *expo-ui/scripts/list-components.js *)
  • expo
  • react-native
  • swiftui
  • jetpack-compose
  • ui-components
  • ios
  • android
  • native
  • cross-platform

README badge

README badge for expo/skills/expo-ui

Renders native UI from React using SwiftUI on iOS and Jetpack Compose on Android via the @expo/ui package. Includes universal cross-platform components (Host, Column, Row, Button, Text, List) for a single code tree, platform-specific APIs for iOS and Android when needed, and drop-in replacements for popular React Native community UI libraries like BottomSheet and DateTimePicker.

Generated from the current SKILL.md.

Does @expo/ui work in Expo Go?
Yes, on SDK 56+ the universal layer runs directly in Expo Go with `npx expo start`. On SDK 55 and earlier, you need to build a dev client with `npx expo run:ios` or `npx expo run:android`.
Can I use @expo/ui with Expo Router?
No. This skill does not cover Expo Router navigation—it focuses only on native UI components and rendering.
Do I have to write separate iOS and Android code?
Not necessarily. The universal components in @expo/ui run the same tree on both platforms. You only split into platform-specific SwiftUI or Jetpack Compose files if the universal layer lacks a component or modifier you need.
Can I migrate from React Native community UI libraries?
Yes. @expo/ui ships drop-in replacements for popular libraries like @gorhom/bottom-sheet and @react-native-community/datetimepicker, imported from @expo/ui/community/<name>.
Does this skill cover custom native modules or Reanimated?
No. This skill is limited to @expo/ui components, modifiers, and drop-in replacements—not custom native code, Reanimated, or data fetching.

Generated from the current SKILL.md. These answers refresh after source changes.