building-native-ui
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseExpo UI Guidelines
Expo UI 指南
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
form-sheet.md Form sheets with footers via Stack and react-native-screens
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+)按需查阅以下资源:
references/
animations.md Reanimated:入场、退场、布局、滚动驱动、手势
controls.md 原生iOS:Switch、Slider、SegmentedControl、DateTimePicker、Picker
form-sheet.md 通过Stack和react-native-screens实现带页脚的表单页
gradients.md 仅新架构支持的experimental_backgroundImage实现CSS渐变
icons.md 通过expo-image使用SF Symbols(sf: 源)、名称、动画、字重
media.md 相机、音频、视频和文件保存
route-structure.md 路由约定、动态路由、分组、文件夹组织
search.md 带头部的搜索栏、useSearch钩子、过滤模式
storage.md SQLite、AsyncStorage、SecureStore
tabs.md NativeTabs、从JS标签页迁移、iOS 26特性
toolbar-and-headers.md Stack头部和工具栏按钮、菜单、搜索(仅iOS)
visual-effects.md 模糊效果(expo-blur)和液态玻璃效果(expo-glass-effect)
webgpu-three.md 使用WebGPU和Three.js实现3D图形、游戏、GPU可视化
zoom-transitions.md Apple缩放:使用Link.AppleZoom实现流畅缩放过渡(iOS 18+)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
重要提示:在创建自定义构建之前,请始终先尝试Expo Go。
大多数Expo应用无需任何自定义原生代码即可在Expo Go中运行。在运行或之前:
npx expo run:iosnpx expo run:android- 从Expo Go开始:运行,使用Expo Go扫描二维码
npx expo start - 检查功能是否可用:在Expo Go中全面测试你的应用
- 仅在必要时创建自定义构建 - 见下文
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
只有在使用以下内容时,你才需要或:
npx expo run:ios/androideas build- 本地Expo模块(中的自定义原生代码)
modules/ - Apple目标(小组件、App Clip、通过实现的扩展)
@bacons/apple-targets - Expo Go未包含的第三方原生模块
- 无法在中配置的自定义原生设置
app.json
When Expo Go Works
Expo Go支持的场景
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.
Expo Go开箱即用地支持大量功能:
- 所有包(相机、定位、通知等)
expo-* - Expo Router导航
- 大多数UI库(reanimated、手势处理程序等)
- 推送通知、深度链接等
如果你不确定,请先尝试Expo Go。 创建自定义构建会增加复杂性、降低迭代速度,并且需要配置Xcode/Android Studio。
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.
- 注意未终止的字符串。确保转义嵌套的反引号;永远不要忘记正确转义引号。
- 始终在文件顶部使用导入语句。
- 文件名始终使用短横线命名法,例如
comment-card.tsx - 在移动或重构导航时,始终删除旧的路由文件
- 文件名中不要使用特殊字符
- 在tsconfig.json中配置路径别名,重构时优先使用别名而非相对导入
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.
有关详细的路由约定,请参阅。
./references/route-structure.md- 路由位于目录中。
app - 不要将组件、类型或工具函数与路由放在同一目录中。这是一种反模式。
- 确保应用始终有一个匹配"/"的路由,它可以在分组路由内部。
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 - with
expo-imagefor SF Symbols, notsource="sf:name"orexpo-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
- 不要使用从React Native中移除的模块,例如Picker、WebView、SafeAreaView或AsyncStorage
- 不要使用旧版expo-permissions
- 使用而非
expo-audioexpo-av - 使用而非
expo-videoexpo-av - 使用并通过
expo-image加载SF Symbols,而非source="sf:name"或expo-symbols@expo/vector-icons - 使用而非React Native的SafeAreaView
react-native-safe-area-context - 使用而非
process.env.EXPO_OSPlatform.OS - 使用而非
React.useReact.useContext - 使用的Image组件而非原生
expo-image元素img - 使用实现液态玻璃背景
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()
- 始终将根组件包裹在滚动视图中以实现响应式
- 使用而非
<ScrollView contentInsetAdjustmentBehavior="automatic" />,以实现更智能的安全区域内边距<SafeAreaView> - 也应应用于FlatList和SectionList
contentInsetAdjustmentBehavior="automatic" - 使用flexbox而非Dimensions API
- 始终优先使用而非
useWindowDimensions来测量屏幕尺寸Dimensions.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
- 在iOS上有条件地使用expo-haptics,打造更愉悦的体验
- 使用内置触觉反馈的视图,例如React Native的和
<Switch />@react-native-community/datetimepicker - 当路由属于Stack时,它的第一个子元素几乎总是应设置的ScrollView
contentInsetAdjustmentBehavior="automatic" - 优先在Stack.Screen的options中使用来添加搜索栏
headerSearchBarOptions - 在包含可复制数据的文本上使用属性
<Text selectable /> - 考虑将大数字格式化为1.4M或38k之类的形式
- 除非在webview或Expo DOM组件中,否则不要使用或
img等原生元素div
Styling
样式设计
Follow Apple Human Interface Guidelines.
遵循Apple人机界面指南。
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
- 优先使用flex gap而非margin和padding样式
- 尽可能优先使用padding而非margin
- 始终考虑安全区域,可通过Stack头部、标签页或ScrollView/FlatList的来实现
contentInsetAdjustmentBehavior="automatic" - 确保同时考虑顶部和底部的安全区域内边距
- 除非复用样式能提高性能,否则使用内联样式而非StyleSheet.create
- 为状态变化添加入场和退场动画
- 圆角优先使用,除非要创建胶囊形状
{ borderCurve: 'continuous' } - 始终使用导航栈标题,而非页面上的自定义文本元素
- 为ScrollView添加内边距时,使用的padding和gap,而非直接在ScrollView上设置padding(减少裁剪)
contentContainerStyle - 不支持CSS和Tailwind - 使用内联样式
Text Styling
文本样式
- Add the prop to every
selectableelement displaying important data or error messages<Text/> - Counters should use for alignment
{ fontVariant: 'tabular-nums' }
- 为每个显示重要数据或错误消息的元素添加
<Text/>属性selectable - 计数器应使用以保证对齐
{ 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.
使用CSS的样式属性。永远不要使用旧版React Native的shadow或elevation样式。
boxShadowtsx
<View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />支持内阴影。
Navigation
导航
Link
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>使用'expo-router'中的在路由之间导航。
<Link href="/path" />tsx
import { Link } from 'expo-router';
// 基础链接
<Link href="/path" />
// 包裹自定义组件
<Link href="/path" asChild>
<Pressable>...</Pressable>
</Link>尽可能包含以遵循iOS惯例。经常添加上下文菜单和预览以增强导航体验。
<Link.Preview>Stack
Stack
- ALWAYS use files to define stacks
_layout.tsx - Use Stack from 'expo-router/stack' for native navigation stacks
- 始终使用文件定义栈
_layout.tsx - 使用'expo-router/stack'中的Stack实现原生导航栈
Page Title
页面标题
Set the page title in Stack.Screen options:
tsx
<Stack.Screen options={{ title: "Home" }} />在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组件添加长按上下文菜单:
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.
经常使用链接预览以增强导航体验:
tsx
<Link href="/settings">
<Link.Trigger>
<Pressable>
<Card />
</Pressable>
</Link.Trigger>
<Link.Preview />
</Link>链接预览可与上下文菜单一起使用。
Modal
模态框
Present a screen as a modal:
tsx
<Stack.Screen name="modal" options={{ presentation: "modal" }} />Prefer this to building a custom modal component.
以模态框形式展示页面:
tsx
<Stack.Screen name="modal" options={{ presentation: "modal" }} />优先使用这种方式而非构建自定义模态框组件。
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" }
以动态表单页形式展示页面:
tsx
<Stack.Screen
name="sheet"
options={{
presentation: "formSheet",
sheetGrabberVisible: true,
sheetAllowedDetents: [0.5, 1.0],
contentStyle: { backgroundColor: "transparent" },
}}
/>- 使用可在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>
);
}每个标签页内包含标签页和栈的标准应用布局:
app/
_layout.tsx — <NativeTabs />
(index,search)/
_layout.tsx — <Stack />
index.tsx — 主列表
search.tsx — 搜索视图tsx
// 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>
);
}创建共享分组路由,以便两个标签页都能推送通用页面:
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>
);
}