【免费下载链接】autoskills
One command. Your entire AI skill stack. Installed.
在 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 54 | SDK 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 Tabs | Native 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" } } }常见问题排查清单
- Android 上图标不显示:SDK 55 添加
md属性,或改用 VectorIcon; - 头部缺失:在每个标签组内嵌套一个 Stack;
- Trigger 名称不匹配:
name必须与路由名完全一致,包括括号; - 徽标不可见:Badge 必须是 Trigger 的子组件,而不是属性;
- iOS 18 及更早版本标签栏透明:如果页面使用
ScrollView或FlatList,确保它是页面组件的第一个不透明子元素;若必须包在另一个View中,该包装层要设置collapsable={false};如果页面不用ScrollView/FlatList,则在NativeTabs.Trigger选项里设置disableTransparentOnScrollEdge={true}让标签栏不透明; - 点击标签不能滚动回顶部:确认当前激活标签的 Trigger 未设置
disableScrollToTop,且ScrollView是页面组件的第一个子元素; - 切换标签时头部按钮闪烁:确保应用外层包裹了
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.
相关推荐
Expo Router NativeTabs 完全指南:在 Expo SDK 54/55 上构建原生标签导航
Expo Router NativeTabs 完全指南:在 Expo SDK 54/55 上构建原生标签导航 本篇技术指南以 AAS(agentic aweso
AI 技能AI 插件expo-glass-effect 深度指南:在 Expo 应用中使用 iOS 26 Liquid Glass 原生毛玻璃效果
expo glass effect 深度指南:在 Expo 应用中使用 iOS 26 Liquid Glass 原生毛玻璃效果 expo glass effec
移动开发前端跨平台原生移动Nx 工作区中 Expo SDK 53 升级迁移到 SDK 54 的完整指南
Nx 工作区中 Expo SDK 53 升级迁移到 SDK 54 的完整指南 导读:本文以 Nx 官方仓库 ai instructions for expo 5
开发工具构建工具MonorepoCLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考