building-native-ui
Original:🇺🇸 English
Not Translated
Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.
1installs
Sourcekorsoftwaresolutions/ui
Added on
NPX Install
npx skill4agent add korsoftwaresolutions/ui building-native-uiSKILL.md Content
Expo UI Guidelines
References
Consult these resources as needed:
- ./references/route-structure.md -- Route file conventions, dynamic routes, query parameters, groups, and folder organization
- ./references/tabs.md -- Native tab bar with NativeTabs, migration from JS tabs, iOS 26 features
- ./references/icons.md -- SF Symbols with expo-symbols, common icon names, animations, and weights
- ./references/controls.md -- Native iOS controls: Switch, Slider, SegmentedControl, DateTimePicker, Picker
- ./references/visual-effects.md -- Blur effects with expo-blur and liquid glass with expo-glass-effect
- ./references/animations.md -- Reanimated animations: entering, exiting, layout, scroll-driven, and gestures
- ./references/search.md -- Search bar integration with headers, useSearch hook, and filtering patterns
- ./references/gradients.md -- CSS gradients using experimental_backgroundImage (New Architecture only)
- ./references/media.md -- Media handling for Expo Router including camera, audio, video, and file saving
- ./references/storage.md -- Data storage patterns including SQLite, AsyncStorage, and SecureStore
- ./references/webgpu-three.md -- 3D graphics, games, and GPU-powered visualizations with WebGPU and Three.js
- ./references/toolbars-and-headers.md -- Customizing stack headers and toolbar with buttons, menus, and search bars in expo-router app. Available only on iOS.
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 or :
npx expo run:iosnpx expo run:android- Start with Expo Go: Run and scan the QR code with Expo Go
npx expo start - 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 or ONLY when using:
npx expo run:ios/androideas build- 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 packages (camera, location, notifications, etc.)
expo-* - 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 - Always remove old route files when moving or restructuring navigation
- Never use special characters in file names
- Configure tsconfig.json with path aliases, and prefer aliases over relative imports for refactors.
Routes
See for detailed route conventions.
./references/route-structure.md- Routes belong in the directory.
app - Never co-locate components, types, or utilities in the app directory. This is an anti-pattern.
- Ensure the app always has a route that matches "/", it may be inside a group route.
Library Preferences
- Never use modules removed from React Native such as Picker, WebView, SafeAreaView, or AsyncStorage
- Never use legacy expo-permissions
- not
expo-audioexpo-av - not
expo-videoexpo-av - not
expo-symbols@expo/vector-icons - not react-native SafeAreaView
react-native-safe-area-context - not
process.env.EXPO_OSPlatform.OS - not
React.useReact.useContext - Image component instead of intrinsic element
expo-imageimg - for liquid glass backdrops
expo-glass-effect
Responsiveness
- Always wrap root component in a scroll view for responsiveness
- Use instead of
<ScrollView contentInsetAdjustmentBehavior="automatic" />for smarter safe area insets<SafeAreaView> - should be applied to FlatList and SectionList as well
contentInsetAdjustmentBehavior="automatic" - Use flexbox instead of Dimensions API
- ALWAYS prefer over
useWindowDimensionsto measure screen sizeDimensions.get()
Behavior
- Use expo-haptics conditionally on iOS to make more delightful experiences
- Use views with built-in haptics like from React Native and
<Switch />@react-native-community/datetimepicker - When a route belongs to a Stack, its first child should almost always be a ScrollView with set
contentInsetAdjustmentBehavior="automatic" - Prefer in Stack.Screen options to add a search bar
headerSearchBarOptions - Use the prop on text containing data that could be copied
<Text selectable /> - 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 for rounded corners unless creating a capsule shape
{ borderCurve: 'continuous' } - ALWAYS use a navigation stack title instead of a custom text element on the page
- When padding a ScrollView, use padding and gap instead of padding on the ScrollView itself (reduces clipping)
contentContainerStyle - CSS and Tailwind are not supported - use inline styles
Text Styling
- Add the prop to every
selectableelement displaying important data or error messages<Text/> - Counters should use for alignment
{ fontVariant: 'tabular-nums' }
Shadows
Use CSS style prop. NEVER use legacy React Native shadow or elevation styles.
boxShadowtsx
<View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />'inset' shadows are supported.
Navigation
Link
Use from 'expo-router' for navigation between routes.
<Link href="/path" />tsx
import { Link } from 'expo-router';
// Basic link
<Link href="/path" />
// Wrapping custom components
<Link href="/path" asChild>
<Pressable>...</Pressable>
</Link>Whenever possible, include a to follow iOS conventions. Add context menus and previews frequently to enhance navigation.
<Link.Preview>Stack
- ALWAYS use files to define stacks
_layout.tsx - Use Stack from 'expo-router/stack' for native navigation stacks
Page Title
Set the page title in Stack.Screen options:
tsx
<Stack.Screen options={{ title: "Home" }} />Context Menus
Add long press context menus to Link components:
tsx
import { 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>;Link Previews
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.
Modal
Present a screen as a modal:
tsx
<Stack.Screen name="modal" options={{ presentation: "modal" }} />Prefer this to building a custom modal component.
Sheet
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" },
}}
/>- Using makes the background liquid glass on iOS 26+.
contentStyle: { backgroundColor: "transparent" }
Common route structure
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, Icon, Label } from "expo-router/unstable-native-tabs";
import { Theme } from "../components/theme";
export default function Layout() {
return (
<Theme>
<NativeTabs>
<NativeTabs.Trigger name="(index)">
<Icon sf="list.dash" />
<Label>Items</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(search)" role="search" />
</NativeTabs>
</Theme>
);
}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 { PlatformColor } from "react-native";
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: 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>
);
}