building-ui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Expo UI Guidelines

Expo UI 指南

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/route-structure.md -- 路由文件约定、动态路由、查询参数、路由组以及文件夹组织结构
  • ./references/tabs.md -- 基于NativeTabs的原生标签栏、从JS标签栏迁移、iOS 26相关特性
  • ./references/icons.md -- 结合expo-symbols使用SF Symbols、常用图标名称、动画及权重设置
  • ./references/controls.md -- 原生iOS控件:Switch、Slider、SegmentedControl、DateTimePicker、Picker
  • ./references/visual-effects.md -- 使用expo-blur实现模糊效果、使用expo-glass-effect实现液态玻璃效果
  • ./references/animations.md -- Reanimated动画:入场、退场、布局、滚动驱动及手势动画
  • ./references/search.md -- 搜索栏与头部集成、useSearch钩子及过滤模式
  • ./references/gradients.md -- 使用experimental_backgroundImage实现CSS渐变(仅支持新架构)
  • ./references/media.md -- Expo Router中的媒体处理,包括相机、音频、视频及文件保存
  • ./references/storage.md -- 数据存储模式,包括SQLite、AsyncStorage及SecureStore
  • ./references/webgpu-three.md -- 使用WebGPU和Three.js实现3D图形、游戏及GPU驱动的可视化效果

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专属目标(通过
    @bacons/apple-targets
    实现的小组件、App Clip、扩展功能)
  • 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、gesture handler等)
  • 推送通知、深度链接等
若不确定,先尝试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
    目录下。
  • 切勿在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-symbols
    not
    @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-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
  • 始终考虑安全区域,可通过栈头部、标签栏或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

链接

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

栈导航

  • 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: "首页" }} />

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="分享"
      icon="square.and.arrow.up"
      onPress={handleSharePress}
    />
    <Link.MenuAction
      title="屏蔽"
      icon="nosign"
      destructive
      onPress={handleBlockPress}
    />
    <Link.Menu title="更多" icon="ellipsis">
      <Link.MenuAction title="复制" icon="doc.on.doc" onPress={() => {}} />
      <Link.MenuAction
        title="删除"
        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>项目</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: "项目", 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>
  );
}