Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-16 | ✗→✓ | ▲ Improved | 78% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 45% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 41% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 89% | 0% |
Use this skill when you need complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.
Consult these resources as needed:
references/
animations.md Reanimated: entering, exiting, layout, scroll-driven, gestures
controls.md Native iOS: Switch, Slider, SegmentedControl, DateTimePicker, Picker
form-sheet.md Form sheets in expo-router: configuration, footers and background interaction.
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
route-structure.md Route conventions, dynamic routes, groups, folder organization
search.md Search bar with headers, useSearch hook, filtering patterns
storage.md SQLite, AsyncStorage, SecureStore
tabs.md NativeTabs, migration from JS tabs, iOS 26 features
toolbar-and-headers.md Stack headers and toolbar buttons, menus, search (iOS only)
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
zoom-transitions.md Apple Zoom: fluid zoom transitions with Link.AppleZoom (iOS 18+)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:
npx expo start and scan the QR code with Expo GoYou need npx expo run:ios/android or eas build ONLY when using:
modules/)@bacons/apple-targets)app.jsonExpo Go supports a huge range of features out of the box:
expo-* packages (camera, location, notifications, etc.)If you're unsure, try Expo Go first. Creating custom builds adds complexity, slower iteration, and requires Xcode/Android Studio setup.
comment-card.tsxSee ./references/route-structure.md for detailed route conventions.
app directory.expo-audio not expo-avexpo-video not expo-avexpo-image with source="sf:name" for SF Symbols, not expo-symbols or @expo/vector-iconsreact-native-safe-area-context not react-native SafeAreaViewprocess.env.EXPO_OS not Platform.OSReact.use not React.useContextexpo-image Image component instead of intrinsic element imgexpo-glass-effect for liquid glass backdropsColor from expo-router for native semantic colors, not raw PlatformColor (type-safe, auto-adapts to light/dark)@react-navigation/* directly — use expo-router/react-navigation instead (covers @react-navigation/native, /core, /elements, /routers)<ScrollView contentInsetAdjustmentBehavior="automatic" /> instead of <SafeAreaView> for smarter safe area insetscontentInsetAdjustmentBehavior="automatic" should be applied to FlatList and SectionList as welluseWindowDimensions over Dimensions.get() to measure screen size<Switch /> from React Native and @react-native-community/datetimepickercontentInsetAdjustmentBehavior="automatic" setScrollView to the page it should almost always be the first component inside the route componentheaderSearchBarOptions in Stack.Screen options to add a search bar<Text selectable /> prop on text containing data that could be copiedFollow Apple Human Interface Guidelines.
contentInsetAdjustmentBehavior="automatic"{ borderCurve: 'continuous' } for rounded corners unless creating a capsule shapecontentContainerStyle padding and gap instead of padding on the ScrollView itself (reduces clipping)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:
tsx// theme/colors.ts 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", })!, };
tsximport { colors } from "@/theme/colors"; <View style={{ backgroundColor: colors.systemBackground }}> <Text style={{ color: colors.label }}>Title</Text> </View>;
useColorScheme() inside any component that renders them so it re-renders when the theme flips (required when React Compiler memoizes the component).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.selectable prop to every <Text/> element displaying important data or error messages{ fontVariant: 'tabular-nums' } for alignmentUse CSS boxShadow style prop. NEVER use legacy React Native shadow or elevation styles.
tsx<View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />
'inset' shadows are supported.
Use <Link href="/path" /> from 'expo-router' for navigation between routes.
tsximport { Link } from 'expo-router'; // Basic link <Link href="/path" /> // Wrapping custom components <Link href="/path" asChild> <Pressable>...</Pressable> </Link>
Whenever possible, include a <Link.Preview> to follow iOS conventions. Add context menus and previews frequently to enhance navigation.
_layout.tsx files to define stacksSet the page title in Stack.Screen options:
tsx<Stack.Screen options={{ title: "Home" }} />
Add long press context menus to Link components:
tsximport { Link } from "expo-router"; <Link href="/settings" asChild> <Link.Trigger> <Pressable> <Card /> </Pressable> </Link.Trigger> <Link.Menu> <Link.MenuAction title="Share" icon="square.and.arrow.up" onPress={handleSharePress} /> <Link.MenuAction title="Block" icon="nosign" destructive onPress={handleBlockPress} /> <Link.Menu title="More" icon="ellipsis"> <Link.MenuAction title="Copy" icon="doc.on.doc" onPress={() => {}} /> <Link.MenuAction title="Delete" icon="trash" destructive onPress={() => {}} /> </Link.Menu> </Link.Menu> </Link>;
Use link previews frequently to enhance navigation:
tsx<Link href="/settings"> <Link.Trigger> <Pressable> <Card /> </Pressable> </Link.Trigger> <Link.Preview /> </Link>
Link preview can be used with context menus.
Present a screen as a modal:
tsx<Stack.Screen name="modal" options={{ presentation: "modal" }} />
Prefer this to building a custom modal component.
Present a screen as a dynamic form sheet:
tsx<Stack.Screen name="sheet" options={{ presentation: "formSheet", sheetGrabberVisible: true, sheetAllowedDetents: [0.5, 1.0], contentStyle: { backgroundColor: "transparent" }, }} />
contentStyle: { backgroundColor: "transparent" } makes the background liquid glass on iOS 26+.A standard app layout with tabs and stacks inside each tab:
app/
_layout.tsx — <NativeTabs />
(index,search)/
_layout.tsx — <Stack />
index.tsx — Main list
search.tsx — Search viewtsx// app/_layout.tsx import { NativeTabs } from "expo-router/unstable-native-tabs"; import { ThemeProvider, DarkTheme, DefaultTheme } from "expo-router/react-navigation"; import { useColorScheme } from "react-native"; export default function Layout() { const colorScheme = useColorScheme(); return ( <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}> <NativeTabs> <NativeTabs.Trigger name="(index)"> <NativeTabs.Trigger.Icon sf="list.dash" md="list" /> <NativeTabs.Trigger.Label>Items</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="(search)" role="search" /> </NativeTabs> </ThemeProvider> ); }
Create a shared group route so both tabs can push common screens:
tsx// app/(index,search)/_layout.tsx import { Stack } from "expo-router/stack"; import { colors } from "@/theme/colors"; export default function Layout({ segment }) { const screen = segment.match(/\((.*)\)/)?.[1]!; const titles: Record<string, string> = { index: "Items", search: "Search" }; return ( <Stack screenOptions={{ headerTransparent: true, headerShadowVisible: false, headerLargeTitleShadowVisible: false, headerLargeStyle: { backgroundColor: "transparent" }, headerTitleStyle: { color: colors.label }, headerLargeTitle: true, headerBlurEffect: "none", headerBackButtonDisplayMode: "minimal", }} > <Stack.Screen name={screen} options={{ title: titles[screen] }} /> <Stack.Screen name="i/[id]" options={{ headerLargeTitle: false }} /> </Stack> ); }
Other measured skills in the registry, with their headline benchmark lift.