| name | expo-native-ui |
| description | Framework (OSS). Build beautiful, native-feeling Expo screens. Covers Apple HIG styling, semantic colors, native controls, SF Symbols, media, animations, visual effects, gradients, storage, and responsive layout. For routing and navigation, use the expo-router skill. |
| version | 1.1.1 |
| license | MIT |
Expo Native UI Guidelines
For routes, links, stacks, tabs, modals, sheets, and headers, use the expo-router skill.
References
Consult these resources as needed:
references/
animations.md Reanimated: entering, exiting, layout, scroll-driven, gestures
controls.md Native iOS: Switch, Slider, SegmentedControl, DateTimePicker, Picker
gradients.md CSS gradients via experimental_backgroundImage (New Arch only)
icons.md SF Symbols via expo-image (sf: source), names, animations, weights
media.md Camera, audio, video, and file saving
storage.md SQLite, AsyncStorage, SecureStore
visual-effects.md Blur (expo-blur) and liquid glass (expo-glass-effect)
webgpu-three.md 3D graphics, games, GPU visualizations with WebGPU and Three.js
Running the App
CRITICAL: Always try Expo Go first before creating custom builds.
Most Expo apps work in Expo Go without any custom native code. Before running npx expo run:ios or npx expo run:android:
- Start with Expo Go: Run
npx expo start and scan the QR code with Expo Go
- Check if features work: Test your app thoroughly in Expo Go
- Only create custom builds when required - see below
When Custom Builds Are Required
You need npx expo run:ios/android or eas build ONLY when using:
- Local Expo modules (custom native code in
modules/)
- Apple targets (widgets, app clips, extensions via
@bacons/apple-targets)
- Third-party native modules not included in Expo Go
- Custom native configuration that can't be expressed in
app.json
When Expo Go Works
Expo Go supports a huge range of features out of the box:
- All
expo-* packages (camera, location, notifications, etc.)
- Expo Router navigation
- Most UI libraries (reanimated, gesture handler, etc.)
- Push notifications, deep links, and more
If you're unsure, try Expo Go first. Creating custom builds adds complexity, slower iteration, and requires Xcode/Android Studio setup.
Code Style
- Be cautious of unterminated strings. Ensure nested backticks are escaped; never forget to escape quotes correctly.
- Always use import statements at the top of the file.
- Always use kebab-case for file names, e.g.
comment-card.tsx
- Never use special characters in file names
- Configure tsconfig.json with path aliases, and prefer aliases over relative imports for refactors.
Library Preferences
- Never use modules removed from React Native such as Picker, WebView, SafeAreaView, or AsyncStorage
- Never use legacy expo-permissions
expo-audio not expo-av
expo-video not expo-av
expo-image with source="sf:name" for SF Symbols, not expo-symbols or @expo/vector-icons
react-native-safe-area-context not react-native SafeAreaView
process.env.EXPO_OS not Platform.OS
React.use not React.useContext
expo-image Image component instead of intrinsic element img
expo-glass-effect for liquid glass backdrops
Color from expo-router for native semantic colors, not raw PlatformColor (type-safe, auto-adapts to light/dark)
- In SDK 56+, never import from
@react-navigation/* directly — use expo-router/react-navigation instead (covers @react-navigation/native, /core, /elements, /routers)
Responsiveness
- Always wrap root component in a scroll view for responsiveness
- Use
<ScrollView contentInsetAdjustmentBehavior="automatic" /> instead of <SafeAreaView> for smarter safe area insets
contentInsetAdjustmentBehavior="automatic" should be applied to FlatList and SectionList as well
- Use flexbox instead of Dimensions API
- ALWAYS prefer
useWindowDimensions over Dimensions.get() to measure screen size
Behavior
- Use expo-haptics conditionally on iOS to make more delightful experiences
- Use views with built-in haptics like
<Switch /> from React Native and @react-native-community/datetimepicker
- When a route belongs to a Stack, its first child should almost always be a ScrollView with
contentInsetAdjustmentBehavior="automatic" set
- When adding a
ScrollView to the page it should almost always be the first component inside the route component
- Use the
<Text selectable /> prop on text containing data that could be copied
- Consider formatting large numbers like 1.4M or 38k
- Never use intrinsic elements like 'img' or 'div' unless in a webview or Expo DOM component
Styling
Follow Apple Human Interface Guidelines.
General Styling Rules
- Prefer flex gap over margin and padding styles
- Prefer padding over margin where possible
- Always account for safe area, either with stack headers, tabs, or ScrollView/FlatList
contentInsetAdjustmentBehavior="automatic"
- Ensure both top and bottom safe area insets are accounted for
- Inline styles not StyleSheet.create unless reusing styles is faster
- Add entering and exiting animations for state changes
- Use
{ borderCurve: 'continuous' } for rounded corners unless creating a capsule shape
- ALWAYS use a navigation stack title instead of a custom text element on the page
- When padding a ScrollView, use
contentContainerStyle padding and gap instead of padding on the ScrollView itself (reduces clipping)
- CSS and Tailwind are not supported - use inline styles
Colors
Use the Color API from expo-router for native semantic colors. It is a type-safe wrapper over PlatformColor that exposes iOS UIKit colors through Color.ios.* and Android Material 3 colors through Color.android.material.* (static) or Color.android.dynamic.* (adapts to the user's wallpaper on Android 12+). These resolve on-device and automatically adapt to light/dark mode and accessibility settings, so you no longer maintain separate light/dark hex tables or a colors.web.ts file.
Color is platform-specific, so wrap each value in Platform.select with a default hex fallback for web. Centralize the palette in theme/colors.ts and import colors everywhere:
import { Platform } from 'react-native';
import { Color } from 'expo-router';
export const colors = {
label: Platform.select({
ios: Color.ios.label,
android: Color.android.dynamic.onSurface,
default: '#000000',
})!,
secondaryLabel: Platform.select({
ios: Color.ios.secondaryLabel,
android: Color.android.dynamic.onSurfaceVariant,
default: '#3c3c43',
})!,
separator: Platform.select({
ios: Color.ios.separator,
android: Color.android.dynamic.outlineVariant,
default: '#c6c6c8',
})!,
systemBackground: Platform.select({
ios: Color.ios.systemBackground,
android: Color.android.dynamic.surface,
default: '#ffffff',
})!,
systemBlue: Platform.select({
ios: Color.ios.systemBlue,
android: Color.android.dynamic.primary,
default: '#007aff',
})!,
};
import { colors } from '@/theme/colors';
<View style={{ backgroundColor: colors.systemBackground }}>
<Text style={{ color: colors.label }}>Title</Text>
</View>;
- iOS re-resolves these colors automatically when the system theme changes. On Android, call
useColorScheme() inside any component that renders them so it re-renders when the theme flips (required when React Compiler memoizes the component).
- Don't pass
Color / PlatformColor values into Reanimated styles — use static colors there (see references/animations.md).
Platform.select({...})! returns string | OpaqueColorValue. Most React Native style props accept ColorValue (string | OpaqueColorValue) so this works fine. But some third-party props only accept string (e.g. tintColor on expo-image). Cast when needed: colors.label as string.
Text Styling
- Add the
selectable prop to every <Text/> element displaying important data or error messages
- Counters should use
{ fontVariant: 'tabular-nums' } for alignment
Shadows
Use CSS boxShadow style prop. NEVER use legacy React Native shadow or elevation styles.
<View style={{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }} />
'inset' shadows are supported.