expo-app-design

Complete guide for building beautiful apps with Expo Router. Based on official Expo skills from expo/skills repository.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "expo-app-design" with this command: npx skills add 5dlabs/cto/5dlabs-cto-expo-app-design

Expo App Design

Complete guide for building beautiful apps with Expo Router. Based on official Expo skills from expo/skills repository.

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

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

Code Style

  • Always use kebab-case for file names, e.g. comment-card.tsx

  • Always remove old route files when moving or restructuring navigation

  • Never use special characters in file names

  • Configure tsconfig.json with path aliases, prefer aliases over relative imports

  • Be cautious of unterminated strings and escape nested backticks properly

Routes

  • Routes belong in the app directory

  • Never co-locate components, types, or utilities in the app directory

  • Ensure the app always has a route that matches "/"

Library Preferences

Old/Avoid Use Instead

expo-av

expo-audio and expo-video

expo-permissions

Individual package permission APIs

@expo/vector-icons

expo-symbols (SF Symbols)

AsyncStorage

expo-sqlite/localStorage/install

expo-app-loading

expo-splash-screen

Platform.OS

process.env.EXPO_OS

React.useContext

React.use

Intrinsic img

expo-image Image component

expo-linear-gradient

experimental_backgroundImage

  • CSS gradients

React Native SafeAreaView

react-native-safe-area-context

Responsiveness

  • Always wrap root component in a scroll view for responsiveness

  • Use <ScrollView contentInsetAdjustmentBehavior="automatic" /> instead of <SafeAreaView>

  • Apply contentInsetAdjustmentBehavior="automatic" to FlatList and SectionList

  • Use flexbox instead of Dimensions API

  • ALWAYS prefer useWindowDimensions over Dimensions.get()

Styling (Apple HIG)

  • Prefer flex gap over margin and padding styles

  • Prefer padding over margin where possible

  • Inline styles not StyleSheet.create unless reusing styles

  • Add entering and exiting animations for state changes

  • Use { borderCurve: 'continuous' } for rounded corners unless creating a capsule

  • ALWAYS use a navigation stack title instead of custom text element

  • Use CSS boxShadow style prop, NEVER use legacy React Native shadow or elevation

  • CSS and Tailwind are not supported - use inline styles

// Correct shadow usage <View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />

Text Styling

  • Add selectable prop to <Text/> displaying important data or error messages

  • Counters should use { fontVariant: 'tabular-nums' } for alignment

Navigation

Link

Use <Link href="/path" /> from 'expo-router' for navigation:

import { Link } from 'expo-router';

// Basic link <Link href="/path" />

// Wrapping custom components <Link href="/path" asChild> <Pressable>...</Pressable> </Link>

Include <Link.Preview> to follow iOS conventions for context menus and previews:

<Link href="/settings"> <Link.Trigger> <Pressable><Card /></Pressable> </Link.Trigger> <Link.Preview /> </Link>

Context Menus

<Link href="/settings" asChild> <Link.Trigger> <Pressable><Card /></Pressable> </Link.Trigger> <Link.Menu> <Link.MenuAction title="Share" icon="square.and.arrow.up" onPress={handleShare} /> <Link.MenuAction title="Block" icon="nosign" destructive onPress={handleBlock} /> <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>

Modal

<Stack.Screen name="modal" options={{ presentation: "modal" }} />

Sheet

<Stack.Screen name="sheet" options={{ presentation: "formSheet", sheetGrabberVisible: true, sheetAllowedDetents: [0.5, 1.0], contentStyle: { backgroundColor: "transparent" }, // Liquid glass on iOS 26+ }} />

Native Tabs (SDK 54+)

Always prefer NativeTabs from 'expo-router/unstable-native-tabs':

import { NativeTabs, Icon, Label, Badge } from "expo-router/unstable-native-tabs";

export default function TabLayout() { return ( <NativeTabs minimizeBehavior="onScrollDown"> <NativeTabs.Trigger name="index"> <Label>Home</Label> <Icon sf="house.fill" /> <Badge>9+</Badge> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Icon sf="gear" /> <Label>Settings</Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="(search)" role="search"> <Label>Search</Label> </NativeTabs.Trigger> </NativeTabs> ); }

NativeTabs Rules

  • Include a trigger for each tab

  • name must match route name exactly (including parentheses)

  • Place search tab last to combine with search bar

  • Use role prop for common tab types: search , favorites , more , etc.

Icon Component

<Icon sf="house.fill" /> // SF Symbol only <Icon sf="house.fill" drawable="ic_home" /> // With Android drawable <Icon src={require('./icon.png')} /> // Custom image <Icon sf={{ default: "house", selected: "house.fill" }} /> // State variants

Badge Component

<Badge>9+</Badge> // Numeric badge <Badge /> // Dot indicator

SF Symbols (expo-symbols)

Use SF Symbols for native feel. Never use FontAwesome or Ionicons.

import { SymbolView } from "expo-symbols"; import { PlatformColor } from "react-native";

<SymbolView tintColor={PlatformColor("label")} resizeMode="scaleAspectFit" name="square.and.arrow.down" style={{ width: 16, height: 16 }} />

Common Icons

Category Icons

Navigation house.fill , gear , magnifyingglass , plus , xmark , chevron.left/right

Media play.fill , pause.fill , stop.fill , speaker.wave.2.fill

Social heart , heart.fill , star , star.fill , person , person.fill

Actions square.and.arrow.up (share), doc.on.doc (copy), trash (delete), pencil (edit)

Status checkmark.circle.fill , xmark.circle.fill , exclamationmark.triangle , bell.fill

Animated Symbols

<SymbolView name="checkmark.circle" animationSpec={{ effect: { type: "bounce", direction: "up" } }} />

Effects: bounce , pulse , variableColor , scale

Symbol Weights

ultraLight , thin , light , regular , medium , semibold , bold , heavy , black

Common Route Structure

app/ _layout.tsx — <NativeTabs /> (index,search)/ _layout.tsx — <Stack /> index.tsx — Main list search.tsx — Search view

// app/_layout.tsx import { NativeTabs, Icon, Label } from "expo-router/unstable-native-tabs";

export default function Layout() { return ( <NativeTabs> <NativeTabs.Trigger name="(index)"> <Icon sf="list.dash" /> <Label>Items</Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="(search)" role="search" /> </NativeTabs> ); }

// app/(index,search)/_layout.tsx - Shared stack for both tabs import { Stack } from "expo-router/stack"; import { PlatformColor } from "react-native";

export default function Layout({ segment }) { const screen = segment.match(/((.*)\)/)?.[1]!; const titles = { index: "Items", search: "Search" };

return ( <Stack screenOptions={{ headerTransparent: true, headerShadowVisible: false, headerLargeTitleShadowVisible: false, headerLargeStyle: { backgroundColor: "transparent" }, headerTitleStyle: { color: PlatformColor("label") }, headerLargeTitle: true, headerBlurEffect: "none", headerBackButtonDisplayMode: "minimal", }} > <Stack.Screen name={screen} options={{ title: titles[screen] }} /> <Stack.Screen name="i/[id]" options={{ headerLargeTitle: false }} /> </Stack> ); }

Behavior

  • Use expo-haptics conditionally on iOS for delightful experiences

  • Use views with built-in haptics like <Switch /> and @react-native-community/datetimepicker

  • When a route belongs to a Stack, its first child should be a ScrollView with contentInsetAdjustmentBehavior="automatic"

  • Prefer headerSearchBarOptions in Stack.Screen options to add a search bar

  • Use <Text selectable /> for data that could be copied

  • Format large numbers (1.4M, 38k)

  • Never use intrinsic elements like 'img' or 'div' unless in webview or DOM component

Visual Effects

Blur Effects (expo-blur)

import { BlurView } from 'expo-blur';

<BlurView intensity={50} style={StyleSheet.absoluteFill} />

Liquid Glass (expo-glass-effect)

import { GlassView } from 'expo-glass-effect';

<GlassView style={styles.glass} cornerRadius={16} />

Validation Commands

npx tsc --noEmit # Type check npx eslint . # Lint npm test # Tests npx expo-doctor # Doctor check npx expo start # Start dev

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

expo-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

better-auth-expo

No summary provided by upstream source.

Repository SourceNeeds Review
General

elysia-llm-docs

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend-excellence

No summary provided by upstream source.

Repository SourceNeeds Review