☰
基于 Expo Router 的 NativeTabs 原生标签导航完全指南:从 SDK 54 迁移到 iOS 26 Liquid Glass
2026/10/9 2:41:03 网站建设 项目流程

【免费下载链接】autoskills

One command. Your entire AI skill stack. Installed.

项目地址:https://gitcode.com/gh_mirrors/au/autoskills
点击查看免费下载

在 Expo 生态中构建移动端标签导航时,expo-router/unstable-native-tabs提供的NativeTabs是打造最佳 iOS 体验的首选方案。本文以 autoskills 仓库中 building-native-ui 技能包的核心参考文档 tabs.md 为主体,系统讲解NativeTabs的组件化 API、SDK 54 与 SDK 55+ 的语法差异、iOS 26 Liquid Glass 新特性,以及从 JS Tabs 迁移的完整路径。读完本文,你将掌握原生标签栏的声明式写法、图标与徽标配置、安全区域处理、搜索标签集成等全部实战能力,并能对照仓库中的技能源码与测试用例验证每一个用法。

SDK 兼容性:SDK 54 与 SDK 55+ 的语法分水岭

NativeTabs要求SDK 54 及以上,推荐 SDK 55。两个大版本之间的 API 形态有显著差异,是迁移时最容易踩坑的地方:

方面SDK 54SDK 55+
导入方式import { NativeTabs, Icon, Label, Badge, VectorIcon }仅import { NativeTabs }
图标<Icon sf="house.fill" /><NativeTabs.Trigger.Icon sf="house.fill" />
标签文本<Label>Home</Label><NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
徽标<Badge>9+</Badge><NativeTabs.Trigger.Badge>9+</NativeTabs.Trigger.Badge>
Android 图标drawable属性md属性(Material Symbols)

SDK 55 将Icon、Label、Badge全部收敛为NativeTabs.Trigger的子组件,形成统一的组件树。文中所有示例默认使用 SDK 55 语法;若你在 SDK 54 上运行,只需把NativeTabs.Trigger.Icon/Label/Badge替换为独立的Icon、Label、Badge导入即可。

基本用法:声明式的原生标签栏

标签布局通过app/(tabs)/_layout.tsx(或直接app/_layout.tsx)中的NativeTabs定义。每个标签对应一个NativeTabs.Trigger,其name必须与路由名精确匹配(含括号):

import { NativeTabs } from "expo-router/unstable-native-tabs"; export default function TabLayout() { return ( <NativeTabs minimizeBehavior="onScrollDown"> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Icon sf="house.fill" md="home" /> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Badge>9+</NativeTabs.Trigger.Badge> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Icon sf="gear" md="settings" /> <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="(search)" role="search"> <NativeTabs.Trigger.Label>Search</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

这段代码同时为 iOS 指定了 SF Symbol(sf)、为 Android 指定了 Material Symbol(md),并演示了带徽标、普通、搜索三种 Trigger 形态。在 autoskills 仓库的技能 SKILL.md 中,也给出了配套的完整路由结构示例:app/_layout.tsx放NativeTabs,每个 tab 内再用Stack承载各自页面与导航栈。

使用规则:四条必须遵守的约定

  • 每个标签都必须有对应 Trigger,缺少任何一个都会导致路由无法到达;
  • NativeTabs.Trigger的name必须与路由名完全一致,包括括号,例如<NativeTabs.Trigger name="(search)">;
  • 搜索标签尽量放在列表末尾,这样它能与搜索栏自然结合(iOS 上搜索栏会与末尾标签联动);
  • 对常见标签类型使用role属性表达语义;
  • 标签必须是静态的——不允许在运行时动态增删标签,否则会重新挂载导航器并丢失页面状态。

平台特性:各平台的原生实现

NativeTabs之所以优于 JS 实现的Tabs,核心在于它直接调用平台自带的标签栏组件:

  • iOS 26+:自动获得 Liquid Glass(液态玻璃)效果与系统级原生外观;
  • Android:使用 Material 3 底部导航(bottom navigation);
  • 整体带来更好的性能与更地道的原生手感。

这与 building-native-ui 技能包整体遵循 Apple Human Interface Guidelines、优先原生体验的定位一致。

Icon 组件:SF Symbols 与 Material Symbols 双端配置

NativeTabs.Trigger.Icon支持以下配置方式:

// SF Symbol (iOS) + Material Symbol (Android) <NativeTabs.Trigger.Icon sf="house.fill" md="home" /> // 状态变体:未选中/选中使用不同符号 <NativeTabs.Trigger.Icon sf={{ default: "house", selected: "house.fill" }} md="home" /> // 自定义图片 <NativeTabs.Trigger.Icon src={require('./icon.png')} /> // Xcode asset catalog —— 仅 iOS(SDK 55+) <NativeTabs.Trigger.Icon xcasset="home-icon" /> <NativeTabs.Trigger.Icon xcasset={{ default: "home-outline", selected: "home-filled" }} /> // 渲染模式 —— 仅 iOS(SDK 55+) <NativeTabs.Trigger.Icon src={require('./icon.png')} renderingMode="template" /> <NativeTabs.Trigger.Icon src={require('./gradient.png')} renderingMode="original" />

renderingMode的语义:"template"会应用 tint 颜色(适用于单色图标),"original"保留源图颜色(适用于渐变等多彩素材);Android 端始终按 original 处理。

关于图标库选型,技能包的态度非常明确:优先使用 SF Symbols 而非第三方矢量图标库。参考文档 icons.md 中强调 "Never use FontAwesome or Ionicons",并在 SKILL.md 的库偏好清单里指定用expo-image的source="sf:name"呈现 SF Symbols。符号命名使用点分记法(如square.and.arrow.up),常用的导航、媒体、社交、状态类符号清单都可以在 icons.md 中查到。

Label 与 Badge:文本与徽标

// Label <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Label hidden>Home</NativeTabs.Trigger.Label> {/* 仅图标的标签 */} // Badge <NativeTabs.Trigger.Badge>9+</NativeTabs.Trigger.Badge> <NativeTabs.Trigger.Badge /> {/* 纯圆点指示器 */}

hidden让标签只显示图标;Badge不带子元素时退化为一个圆点指示器,适合表达"有新内容"这类弱提示。

iOS 26 特性:Liquid Glass 与新交互

Liquid Glass 标签栏

在 iOS 26+ 上,标签栏自动采用 Liquid Glass 外观,无需额外配置。

滚动时收起

<NativeTabs minimizeBehavior="onScrollDown">

页面内容向下滚动时标签栏自动收起,为内容腾出更多空间,这是 iOS 26 的招牌交互之一。

搜索标签

<NativeTabs.Trigger name="(search)" role="search"> <NativeTabs.Trigger.Label>Search</NativeTabs.Trigger.Label> </NativeTabs.Trigger>

注意:搜索标签放在列表末尾体验最佳。role="search"会让标签栏与搜索功能深度整合,具体到页面内部,可配合headerSearchBarOptions与useSearchHook 实现搜索状态管理,相关完整示例见参考文档 search.md。

Role 属性

为特殊标签类型提供语义化角色:

<NativeTabs.Trigger name="search" role="search" /> <NativeTabs.Trigger name="favorites" role="favorites" /> <NativeTabs.Trigger name="more" role="more" />

可用角色集合:search|more|favorites|bookmarks|contacts|downloads|featured|history|mostRecent|mostViewed|recents|topRated。这些角色会让标签栏获得系统级的位置与行为语义(例如在更多入口聚合溢出项)。

自定义:Tint 颜色与动态颜色

Tint 颜色

<NativeTabs tintColor="#007AFF">

为整个标签栏指定主题色。

动态颜色(iOS)

Liquid Glass 背景下,固定颜色可能显得生硬。使用DynamicColorIOS让颜色随浅色/深色外观自适应:

import { DynamicColorIOS, Platform } from 'react-native'; const adaptiveBlue = Platform.select({ ios: DynamicColorIOS({ light: '#007AFF', dark: '#0A84FF' }), default: '#007AFF', }); <NativeTabs tintColor={adaptiveBlue}>

条件标签:按需显示 Admin 入口

<NativeTabs.Trigger name="admin" hidden={!isAdmin}> <NativeTabs.Trigger.Label>Admin</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon sf="shield.fill" md="shield" /> </NativeTabs.Trigger>

重要约束:不要在标签已经可见后再切换hidden—— 反复切换可见性会重新挂载导航器;只应在首次渲染期间决定显隐。同时记住:被隐藏的标签无法再被导航到(hidden tabs cannot be navigated to)。

行为选项:精细控制点击行为

<NativeTabs.Trigger name="home" disablePopToTop // 点击当前激活标签时不弹回栈顶 disableScrollToTop // 点击当前激活标签时不滚动回顶部 disableAutomaticContentInsets // 放弃自动安全区 inset(SDK 55+) >

三个开关分别对应三种原生默认行为,按需关闭即可。

隐藏标签栏(SDK 55+)

通过NativeTabs上的hidden属性可以动态隐藏整个标签栏:

<NativeTabs hidden={isTabBarHidden}>{/* triggers */}</NativeTabs>

适合阅读模式、全屏播放等需要临时收起标签栏的场景。

底部附件 BottomAccessory(SDK 55+)

NativeTabs.BottomAccessory用于在标签栏上方渲染内容(iOS 26+),典型场景是迷你播放器。它通过usePlacement()在'regular'与'inline'两种布局间自适应。

关键注意点:组件会被同时渲染两份实例(regular 与 inline 布局各一份),因此状态必须存放在组件外部(props、context 或外部 store),不能依赖组件内部 state 持久化。

import { NativeTabs } from "expo-router/unstable-native-tabs"; import { useState } from "react"; import { Pressable, Text, View } from "react-native"; function MiniPlayer({ isPlaying, onToggle, }: { isPlaying: boolean; onToggle: () => void; }) { const placement = NativeTabs.BottomAccessory.usePlacement(); if (placement === "inline") { return ( <Pressable onPress={onToggle}> <SymbolView name={isPlaying ? "pause.fill" : "play.fill"} /> </Pressable> ); } return <View>{/* full player UI */}</View>; } export default function TabLayout() { const [isPlaying, setIsPlaying] = useState(false); return ( <NativeTabs> <NativeTabs.BottomAccessory> <MiniPlayer isPlaying={isPlaying} onToggle={() => setIsPlaying(!isPlaying)} /> </NativeTabs.BottomAccessory> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Icon sf="house.fill" md="home" /> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

这里isPlaying状态被提升到TabLayout,通过 props 传入两个 MiniPlayer 实例,确保两个布局的播放状态始终同步。

安全区域处理(SDK 55+)

SDK 55 起自动处理安全区域:

  • Android:内容自动包裹在 SafeAreaView 中(处理底部 inset);
  • iOS:第一个 ScrollView 自动获得contentInsetAdjustmentBehavior。

若需按标签关闭自动处理,使用disableAutomaticContentInsets并手动管理:

<NativeTabs.Trigger name="index" disableAutomaticContentInsets> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger>
// 在页面内 import { SafeAreaView } from "react-native-screens/experimental"; export default function HomeScreen() { return ( <SafeAreaView edges={{ bottom: true }} style={{ flex: 1 }}> {/* content */} </SafeAreaView> ); }

顺带一提,技能包在 SKILL.md 中建议日常开发优先使用<ScrollView contentInsetAdjustmentBehavior="automatic" />替代 SafeAreaView 来获得更智能的安全区 inset,这与 NativeTabs 的自动处理机制是同一套设计哲学。

使用 Vector Icons(兜底方案)

只有在必须使用@expo/vector-icons(而非 SF Symbols)时才这样做:

import { NativeTabs } from "expo-router/unstable-native-tabs"; import Ionicons from "@expo/vector-icons/Ionicons"; <NativeTabs.Trigger name="home"> <NativeTabs.Trigger.VectorIcon vector={Ionicons} name="home" /> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger>

推荐做法:优先使用sf+md组合而非矢量图标,以获得最原生的观感;SDK 55+ 务必用md属性指定 Android 端使用的 Material Symbols。

与 Stack 组合的结构:为每个标签内嵌导航栈

NativeTabs本身不渲染页面头部。需要导航头部时,在每个标签组内嵌套Stack:

// app/(tabs)/_layout.tsx import { NativeTabs } from "expo-router/unstable-native-tabs"; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="(home)"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon sf="house.fill" md="home" /> </NativeTabs.Trigger> </NativeTabs> ); } // app/(tabs)/(home)/_layout.tsx import Stack from "expo-router/stack"; export default function HomeStack() { return ( <Stack> <Stack.Screen name="index" options={{ title: "Home", headerLargeTitle: true }} /> <Stack.Screen name="details" options={{ title: "Details" }} /> </Stack> ); }

"标签内嵌 Stack"是技能包反复强调的架构范式:参考文档 route-structure.md 指出,头部和标题应设置在每个标签内部的 Stack 中,让每个标签拥有独立的头部与独立的历史栈,根布局通常不带头部(在 tab 布局上设置headerShown: false)。若两个标签需要共享页面(如详情页i/[id]),可使用数组路由(index,search)加unstable_settings锚点配置,完整示例见 route-structure.md。

自定义 Web 布局:原生与 Web 分流

NativeTabs面向 iOS/Android。Web 端可借助平台专属文件实现不同布局:

app/ _layout.tsx # NativeTabs for iOS/Android _layout.web.tsx # Headless tabs for web (expo-router/ui)

也可以抽取为组件对:components/app-tabs.tsx+components/app-tabs.web.tsx。这样移动端享受原生标签栏,Web 端使用expo-router/ui的无头标签实现,互不干扰。

从 JS Tabs 迁移:逐行对照

迁移前(JS Tabs)

import { Tabs } from "expo-router"; <Tabs> <Tabs.Screen name="index" options={{ title: "Home", tabBarIcon: ({ color }) => <IconSymbol name="house.fill" color={color} />, tabBarBadge: 3, }} /> </Tabs>;

迁移后(Native Tabs)

import { NativeTabs } from "expo-router/unstable-native-tabs"; <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon sf="house.fill" md="home" /> <NativeTabs.Trigger.Badge>3</NativeTabs.Trigger.Badge> </NativeTabs.Trigger> </NativeTabs>;

关键差异对照表

JS TabsNative Tabs
<Tabs.Screen><NativeTabs.Trigger>
options={{ title }}<NativeTabs.Trigger.Label>
options={{ tabBarIcon }}<NativeTabs.Trigger.Icon>
tabBarBadge选项<NativeTabs.Trigger.Badge>
基于 Props 的 API基于组件的 API
内置头部需要嵌套<Stack>提供头部

限制与约束

  • Android:最多 5 个标签(Material Design 约束);
  • 嵌套:原生标签不能嵌套在其他原生标签内部;
  • 标签栏高度:无法在程序中测量;
  • FlatList 透明问题:使用disableTransparentOnScrollEdge修复;
  • 动态标签:标签必须静态;运行时变更会重新挂载导航器并丢失状态。

Android 键盘处理配置

在app.json中配置软键盘布局模式为resize,避免键盘遮挡底部标签栏:

{ "expo": { "android": { "softwareKeyboardLayoutMode": "resize" } } }

常见问题排查清单

  1. Android 上图标不显示:SDK 55 添加md属性,或改用 VectorIcon;
  2. 头部缺失:在每个标签组内嵌套一个 Stack;
  3. Trigger 名称不匹配:name必须与路由名完全一致,包括括号;
  4. 徽标不可见:Badge 必须是 Trigger 的子组件,而不是属性;
  5. iOS 18 及更早版本标签栏透明:如果页面使用ScrollView或FlatList,确保它是页面组件的第一个不透明子元素;若必须包在另一个View中,该包装层要设置collapsable={false};如果页面不用ScrollView/FlatList,则在NativeTabs.Trigger选项里设置disableTransparentOnScrollEdge={true}让标签栏不透明;
  6. 点击标签不能滚动回顶部:确认当前激活标签的 Trigger 未设置disableScrollToTop,且ScrollView是页面组件的第一个子元素;
  7. 切换标签时头部按钮闪烁:确保应用外层包裹了ThemeProvider。

针对第 7 点,完整修复方案是引入@react-navigation/native的主题提供器,并跟随系统外观切换主题:

import { ThemeProvider, DarkTheme, DefaultTheme, } from "@react-navigation/native"; import { useColorScheme } from "react-native"; import { Stack } from "expo-router"; export default function Layout() { const colorScheme = useColorScheme(); return ( <ThemeProvider theme={colorScheme === "dark" ? DarkTheme : DefaultTheme}> <Stack /> </ThemeProvider> ); }

如果应用只使用单一明/暗主题,可以跳过useColorScheme直接传入对应主题:

import { ThemeProvider, DarkTheme } from "@react-navigation/native"; import { Stack } from "expo-router"; export default function Layout() { return ( <ThemeProvider theme={DarkTheme}> <Stack /> </ThemeProvider> ); }

在 autoskills 中的定位与使用方式

本指南对应的tabs.md是 autoskills 仓库中building-native-ui技能的一部分。该技能在仓库的技能映射 skills-map.ts 中被挂载到expo技术栈下:当你的项目package.json中存在expo依赖时,autoskills 会自动识别并建议安装该技能;技能包的注册信息(来源expo/skills、commitSha 与文件清单)记录在 skills-registry/index.json,安装逻辑的相关测试见 collect.test.ts。

你可以在项目根目录运行npx autoskills(Node.js >= 22),它会扫描项目依赖并自动安装匹配的 AI Agent 技能,之后 Agent 在编写 Expo 应用时即可参考building-native-ui技能下的 SKILL.md 与全部参考文档,其中就包括本文所讲的 tabs.md。技能的其余参考文档(图标 icons.md、路由结构 route-structure.md、搜索 search.md、视觉效果 visual-effects.md 等)可在同一目录下查阅,与本文共同构成一套完整的 Expo 原生 UI 开发指南。

【免费下载链接】autoskills

One command. Your entire AI skill stack. Installed.

项目地址:https://gitcode.com/gh_mirrors/au/autoskills
点击查看免费下载
上一篇:MemOS 获取建议问题(Get Suggestions)接口深度解析:双模式“猜你想问”实现与实战调用指南
下一篇:Ekko Studio App Relay 应用连接中继:从 LAN 直连到云端中转的完整授权与转发机制

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询