expo-router

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Expo Router Navigation

Expo Router 导航

Navigation and routing for Expo Router apps. For screen styling, colors, controls, animations, media, and visual effects, use the
building-native-ui
skill.
Expo Router 应用的导航与路由方案。如需进行屏幕样式设置、颜色调整、控件配置、动画、媒体及视觉效果处理,请使用
building-native-ui
技能。

References

参考资料

Consult these resources as needed:
references/
  route-structure.md     Route conventions, dynamic routes, groups, folder organization
  tabs.md                NativeTabs, migration from JS tabs, iOS 26 features
  toolbar-and-headers.md Stack headers and toolbar buttons, menus, search (iOS only)
  form-sheet.md          Form sheets in expo-router: configuration, footers and background interaction.
  search.md              Search bar with headers, useSearch hook, filtering patterns
  zoom-transitions.md    Apple Zoom: fluid zoom transitions with Link.AppleZoom (iOS 18+)
按需查阅以下资源:
references/
  route-structure.md     路由约定、动态路由、分组、文件夹组织
  tabs.md                NativeTabs、从JS标签页迁移、iOS 26特性
  toolbar-and-headers.md Stack页眉与工具栏按钮、菜单、搜索(仅iOS)
  form-sheet.md          expo-router中的表单面板:配置、页脚与背景交互
  search.md              页眉搜索栏、useSearch钩子、过滤模式
  zoom-transitions.md    Apple Zoom:使用Link.AppleZoom实现流畅缩放过渡(iOS 18+)

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, and prefer aliases over relative imports for refactors.
  • 文件名始终使用kebab-case格式,例如
    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

库偏好

  • Color
    from
    expo-router
    for native semantic colors, not raw
    PlatformColor
    (type-safe, auto-adapts to light/dark). See
    building-native-ui
    for the full color palette pattern.
  • In SDK 56+, never import from
    @react-navigation/*
    directly — use
    expo-router/react-navigation
    instead (covers
    @react-navigation/native
    ,
    /core
    ,
    /elements
    ,
    /routers
    )
  • 使用
    expo-router
    中的
    Color
    获取原生语义化颜色,而非直接使用
    PlatformColor
    (类型安全,可自动适配明暗模式)。完整的调色板模式请参阅
    building-native-ui
  • 在SDK 56及以上版本中,切勿直接从
    @react-navigation/*
    导入模块 —— 请改用
    expo-router/react-navigation
    (涵盖
    @react-navigation/native
    /core
    /elements
    /routers

Behavior

行为规范

  • Prefer
    Stack.SearchBar
    to add a search bar to a screen
  • 优先使用
    Stack.SearchBar
    为屏幕添加搜索栏

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
    文件定义Stack
  • 使用
    expo-router/stack
    中的Stack实现原生导航栈

Page Title

页面标题

Set the page title with
Stack.Title
:
tsx
<Stack.Title>Home</Stack.Title>
通过
Stack.Title
设置页面标题:
tsx
<Stack.Title>Home</Stack.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="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

Link预览

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 } from "expo-router/unstable-native-tabs";
import { ThemeProvider, DarkTheme, DefaultTheme } from "expo-router/react-navigation";
import { useColorScheme } from "react-native";

export default function Layout() {
  const colorScheme = useColorScheme();
  return (
    <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
      <NativeTabs>
        <NativeTabs.Trigger name="(index)">
          <NativeTabs.Trigger.Icon sf="list.dash" md="list" />
          <NativeTabs.Trigger.Label>Items</NativeTabs.Trigger.Label>
        </NativeTabs.Trigger>
        <NativeTabs.Trigger name="(search)" role="search" />
      </NativeTabs>
    </ThemeProvider>
  );
}
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 { colors } from "@/theme/colors";

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: colors.label },
        headerLargeTitle: true,
        headerBlurEffect: "none",
        headerBackButtonDisplayMode: "minimal",
      }}
    >
      <Stack.Screen name={screen} options={{ title: titles[screen] }} />
      <Stack.Screen name="i/[id]" options={{ headerLargeTitle: false }} />
    </Stack>
  );
}
包含标签页且每个标签页内有Stack的标准应用布局:
app/
  _layout.tsx — <NativeTabs />
  (index,search)/
    _layout.tsx — <Stack />
    index.tsx — 主列表
    search.tsx — 搜索视图
tsx
// app/_layout.tsx
import { NativeTabs } from "expo-router/unstable-native-tabs";
import { ThemeProvider, DarkTheme, DefaultTheme } from "expo-router/react-navigation";
import { useColorScheme } from "react-native";

export default function Layout() {
  const colorScheme = useColorScheme();
  return (
    <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
      <NativeTabs>
        <NativeTabs.Trigger name="(index)">
          <NativeTabs.Trigger.Icon sf="list.dash" md="list" />
          <NativeTabs.Trigger.Label>Items</NativeTabs.Trigger.Label>
        </NativeTabs.Trigger>
        <NativeTabs.Trigger name="(search)" role="search" />
      </NativeTabs>
    </ThemeProvider>
  );
}
创建共享分组路由,使两个标签页都能跳转至通用屏幕:
tsx
// app/(index,search)/_layout.tsx
import { Stack } from "expo-router/stack";
import { colors } from "@/theme/colors";

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: colors.label },
        headerLargeTitle: true,
        headerBlurEffect: "none",
        headerBackButtonDisplayMode: "minimal",
      }}
    >
      <Stack.Screen name={screen} options={{ title: titles[screen] }} />
      <Stack.Screen name="i/[id]" options={{ headerLargeTitle: false }} />
    </Stack>
  );
}