building-native-ui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Expo 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
npx expo run:ios
or
npx expo run:android
:
  1. Start with Expo Go: Run
    npx expo start
    and scan the QR code with Expo Go
  2. Check if features work: Test your app thoroughly in Expo Go
  3. Only create custom builds when required - see below
重要提示:在创建自定义构建之前,请始终先尝试Expo Go。
大多数Expo应用无需任何自定义原生代码即可在Expo Go中运行。在运行
npx expo run:ios
npx expo run:android
之前:
  1. 从Expo Go开始:运行
    npx expo start
    ,使用Expo Go扫描二维码
  2. 检查功能是否可用:在Expo Go中全面测试你的应用
  3. 仅在必要时创建自定义构建 - 见下文

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
只有在使用以下内容时,你才需要
npx expo run:ios/android
eas 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
    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.
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
./references/route-structure.md
for detailed route conventions.
  • Routes belong in the
    app
    directory.
  • 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
  • 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
  • 不要使用从React Native中移除的模块,例如Picker、WebView、SafeAreaView或AsyncStorage
  • 不要使用旧版expo-permissions
  • 使用
    expo-audio
    而非
    expo-av
  • 使用
    expo-video
    而非
    expo-av
  • 使用
    expo-image
    并通过
    source="sf:name"
    加载SF Symbols,而非
    expo-symbols
    @expo/vector-icons
  • 使用
    react-native-safe-area-context
    而非React Native的SafeAreaView
  • 使用
    process.env.EXPO_OS
    而非
    Platform.OS
  • 使用
    React.use
    而非
    React.useContext
  • 使用
    expo-image
    的Image组件而非原生
    img
    元素
  • 使用
    expo-glass-effect
    实现液态玻璃背景

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
  • 始终将根组件包裹在滚动视图中以实现响应式
  • 使用
    <ScrollView contentInsetAdjustmentBehavior="automatic" />
    而非
    <SafeAreaView>
    ,以实现更智能的安全区域内边距
  • contentInsetAdjustmentBehavior="automatic"
    也应应用于FlatList和SectionList
  • 使用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
    <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
  • Prefer
    headerSearchBarOptions
    in Stack.Screen options to add a search bar
  • 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
  • 在iOS上有条件地使用expo-haptics,打造更愉悦的体验
  • 使用内置触觉反馈的视图,例如React Native的
    <Switch />
    @react-native-community/datetimepicker
  • 当路由属于Stack时,它的第一个子元素几乎总是应设置
    contentInsetAdjustmentBehavior="automatic"
    的ScrollView
  • 优先在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
    { 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
  • 优先使用flex gap而非margin和padding样式
  • 尽可能优先使用padding而非margin
  • 始终考虑安全区域,可通过Stack头部、标签页或ScrollView/FlatList的
    contentInsetAdjustmentBehavior="automatic"
    来实现
  • 确保同时考虑顶部和底部的安全区域内边距
  • 除非复用样式能提高性能,否则使用内联样式而非StyleSheet.create
  • 为状态变化添加入场和退场动画
  • 圆角优先使用
    { borderCurve: 'continuous' }
    ,除非要创建胶囊形状
  • 始终使用导航栈标题,而非页面上的自定义文本元素
  • 为ScrollView添加内边距时,使用
    contentContainerStyle
    的padding和gap,而非直接在ScrollView上设置padding(减少裁剪)
  • 不支持CSS和Tailwind - 使用内联样式

Text Styling

文本样式

  • Add the
    selectable
    prop to every
    <Text/>
    element displaying important data or error messages
  • Counters should use
    { fontVariant: 'tabular-nums' }
    for alignment
  • 为每个显示重要数据或错误消息的
    <Text/>
    元素添加
    selectable
    属性
  • 计数器应使用
    { fontVariant: 'tabular-nums' }
    以保证对齐

Shadows

阴影

Use 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.
使用CSS的
boxShadow
样式属性。永远不要使用旧版React Native的shadow或elevation样式。
tsx
<View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />
支持内阴影。

Navigation

导航

Link

Link

Use
<Link href="/path" />
from 'expo-router' for navigation between routes.
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
<Link.Preview>
to follow iOS conventions. Add context menus and previews frequently to enhance navigation.
使用'expo-router'中的
<Link href="/path" />
在路由之间导航。
tsx
import { Link } from 'expo-router';

// 基础链接
<Link href="/path" />

// 包裹自定义组件
<Link href="/path" asChild>
  <Pressable>...</Pressable>
</Link>
尽可能包含
<Link.Preview>
以遵循iOS惯例。经常添加上下文菜单和预览以增强导航体验。

Stack

Stack

  • ALWAYS use
    _layout.tsx
    files to define stacks
  • 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
    contentStyle: { backgroundColor: "transparent" }
    makes the background liquid glass on iOS 26+.
以动态表单页形式展示页面:
tsx
<Stack.Screen
  name="sheet"
  options={{
    presentation: "formSheet",
    sheetGrabberVisible: true,
    sheetAllowedDetents: [0.5, 1.0],
    contentStyle: { backgroundColor: "transparent" },
  }}
/>
  • 使用
    contentStyle: { backgroundColor: "transparent" }
    可在iOS 26+上实现液态玻璃背景。

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 view
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>
  );
}
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>
  );
}