This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

原生标签页

编辑页面

了解如何在 Expo Router 中使用原生标签页布局


使用 Expo Router 创建 Liquid Glass 标签页
使用 Expo Router 创建 Liquid Glass 标签页

了解如何使用原生标签页,通过 Expo Router 在 iOS 上创建 Liquid Glass 标签页。

标签页是用于在应用不同部分之间导航的常见方式。在 Expo Router 中,你可以根据需要使用不同的标签页布局。本指南介绍原生标签页。与其他标签页布局不同,原生标签页使用系统原生标签栏。

示例使用 expo-router/native-tabs,该模块在 SDK 58 及更高版本中可用。在 SDK 54 至 57 中,请改用 expo-router/unstable-native-tabs。

有关其他标签页布局,请参阅:

自定义标签页

如果你的应用需要无法通过系统标签页实现的完全自定义设计,请参阅自定义标签页。

JavaScript 标签页

如果你已经在使用 React Navigation 的标签页,请参阅 JavaScript 标签页。

开始使用

你可以使用基于文件的路由创建标签页布局。以下是一个示例文件结构:

src
 app
  _layout.tsx
  index.tsx
  settings.tsx

上述文件结构会生成一个位于屏幕底部的标签栏。标签栏包含两个标签页:Home 和 Settings。

你可以使用 src/app/_layout.tsx 文件,通过标签页定义应用的根布局。此文件是标签栏和每个标签页的主要布局文件。在其中,你可以控制标签栏和每个标签项的外观与行为。

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon sf="house.fill" md="home" /> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Icon sf="gear" md="settings" /> <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

最后,使用以下两个标签页文件构成标签页的内容:src/app/index.tsx 和 src/app/settings.tsx。

src/app/index.tsx and src/app/settings.tsx
import { View, Text, StyleSheet } from 'react-native'; export default function Tab() { return ( <View style={styles.container}> <Text>Tab [Home|Settings]</Text> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, });

名为 index.tsx 的标签页文件是应用加载时的默认标签页。第二个标签页文件 settings.tsx 展示了如何向标签栏添加更多标签页。

自定义标签栏项

如果你想自定义标签栏项,我们建议使用专为此目的设计的组件 API。目前,你可以自定义:

  • 图标:显示在标签栏项中的图标
  • 标签:显示在标签栏项中的标签
  • 徽章:显示在标签栏项中的徽章

图标

你可以使用 Icon 组件自定义显示在标签栏项中的图标。Icon 组件接受用于 Android Material Symbols 的 md 属性、用于 Apple SF Symbols 图标的 sf 属性,或用于自定义图像的 src 属性。

或者,你可以向 sf、xcasset、drawable、md 或 src 属性传入 {default: ..., selected: ...},为默认状态和选中状态指定不同的图标。

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Icon sf={{ default: 'house', selected: 'house.fill' }} md={{ default: 'home', selected: 'home_filled' }} /> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Icon src={require('../../../assets/setting_icon.png')} /> </NativeTabs.Trigger> </NativeTabs> ); }

iOS 上的 Liquid Glass 会根据背景颜色是浅色还是深色自动改变颜色。由于没有用于此目的的回调,因此你需要使用 PlatformColor 或 DynamicColorIOS 设置图标颜色。

src/app/_layout.tsx
import { DynamicColorIOS } from 'react-native'; import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs labelStyle={{ // For the text color color: DynamicColorIOS({ dark: 'white', light: 'black', }), }} // For the selected icon color tintColor={DynamicColorIOS({ dark: 'white', light: 'black', })}> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Icon sf={{ default: 'house', selected: 'house.fill' }} md="home" /> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Icon src={{ default: require('../assets/setting_icon.png'), selected: require('../assets/selected_setting_icon.png'), }} /> </NativeTabs.Trigger> </NativeTabs> ); }

图标渲染模式

在 iOS 上使用 src 或 xcasset 属性设置自定义图像时,你可以使用 renderingMode 属性控制图标的渲染方式:

  • template(默认):图标会作为模板图像渲染,允许 iOS 应用色调颜色。这适用于应与应用配色方案匹配的单色图标
  • original:图标会以保留原始颜色的方式渲染。这适用于带有渐变或多种颜色的图标
src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> {/* Icon with original colors preserved (e.g., for gradient or multi-color icons) */} <NativeTabs.Trigger name="colorful"> <NativeTabs.Trigger.Icon src={require('../../../assets/colorful_icon.png')} renderingMode="original" /> </NativeTabs.Trigger> {/* Icon rendered as a template (default behavior) */} <NativeTabs.Trigger name="simple"> <NativeTabs.Trigger.Icon src={require('../../../assets/simple_icon.png')} renderingMode="template" /> </NativeTabs.Trigger> </NativeTabs> ); }

资源目录图标(iOS)

在 iOS 上,你可以使用 Xcode 资源目录中的图像作为标签页图标,方法是使用 xcasset 属性。当你希望通过 Xcode 的资源目录管理图标,而不是打包图像文件时,此功能非常有用。

传入包含资源名称的字符串,可在默认状态和选中状态下使用相同图标:

app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Icon xcasset="home-icon" /> </NativeTabs.Trigger> </NativeTabs> ); }

要为默认状态和选中状态使用不同的图标,请传入一个对象:

app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Icon xcasset={{ default: 'home-outline', selected: 'home-filled', }} /> </NativeTabs.Trigger> </NativeTabs> ); }

矢量图标

你可以通过向 src 属性传入图像源来渲染图标字体中的图标,例如 react-native-vector-icons 提供的图标。每个图标集都会提供 getImageSourceSync 方法,该方法可将字形栅格化为图像源,然后你可以直接将其传给 src。

这在 Android 上很有用,因为内置的 md 属性只会渲染描边式 Material Symbols。Material Design Icons 等图标字体同时提供描边和填充字形(例如 home-outline 和 home),因此你可以显示不同的默认图标和选中图标。

首先,安装你想使用的图标集以及 @react-native-vector-icons/get-image,后者提供 getImageSourceSync 所依赖的原生模块。下面的示例使用 @react-native-vector-icons/material-design-icons 图标集:

Terminal
- npx expo install @react-native-vector-icons/material-design-icons @react-native-vector-icons/get-image
- yarn expo install @react-native-vector-icons/material-design-icons @react-native-vector-icons/get-image
- pnpm expo install @react-native-vector-icons/material-design-icons @react-native-vector-icons/get-image
- bun expo install @react-native-vector-icons/material-design-icons @react-native-vector-icons/get-image

getImageSourceSync 是同步方法,因此应在模块作用域中计算一次图像源,而不是在每次渲染时计算。将 src 与 sf 结合使用,即可在 Android 上使用矢量图标、在 iOS 上使用 SF Symbols。在 iOS 上,sf 的优先级高于 src;在 Android 上,图标会回退到 src。

src/app/_layout.tsx
import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons'; import { NativeTabs } from 'expo-router/native-tabs'; const homeIcon = MaterialDesignIcons.getImageSourceSync('home', 24, 'black'); const starOutlineIcon = MaterialDesignIcons.getImageSourceSync('star-outline', 24, 'black'); const starIcon = MaterialDesignIcons.getImageSourceSync('star', 24, 'black'); export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> {/* `sf` is used on iOS, `src` (the vector icon) on Android. */} <NativeTabs.Trigger.Icon sf="house" src={homeIcon} /> </NativeTabs.Trigger> <NativeTabs.Trigger name="explore"> {/* Outlined when unselected, filled when selected. */} <NativeTabs.Trigger.Icon sf={{ default: 'star', selected: 'star.fill' }} src={{ default: starOutlineIcon, selected: starIcon }} /> </NativeTabs.Trigger> </NativeTabs> ); }

标签

你可以使用 Label 组件自定义显示在标签栏项中的标签。Label 组件接受作为子元素传入的字符串标签。如果未提供标签,标签栏项会使用路由名称作为标签。

如果你不想显示标签,可以使用 hidden 属性隐藏标签。

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Label hidden /> </NativeTabs.Trigger> </NativeTabs> ); }

徽章

你可以使用 Badge 组件自定义显示在标签栏项中的徽章。徽章是标签页上方的附加标记,适用于显示通知或未读消息数量。

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="messages"> <NativeTabs.Trigger.Badge>9+</NativeTabs.Trigger.Badge> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Badge /> </NativeTabs.Trigger> </NativeTabs> ); }

自定义标签栏

由于原生标签页布局的外观会因平台而异,因此可用的自定义选项也有所不同。有关所有自定义选项,请参阅 NativeTabs 的 API 参考。

高级用法

隐藏标签栏

你可以在 NativeTabs 组件上使用 hidden 属性隐藏标签栏。要针对特定屏幕隐藏标签栏,可以使用 context API 动态设置 hidden 属性。

src/context/TabBarContext.tsx
import { createContext } from 'react'; export const TabBarContext = createContext<{ setIsTabBarHidden: (hidden: boolean) => void; }>({ setIsTabBarHidden: () => {}, });
src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; import { useState } from 'react'; import { TabBarContext } from '@/context/TabBarContext'; export default function TabLayout() { const [isTabBarHidden, setIsTabBarHidden] = useState(false); return ( <TabBarContext value={{ setIsTabBarHidden }}> <NativeTabs hidden={isTabBarHidden}> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> </TabBarContext> ); }
src/app/index.tsx
import { useFocusEffect } from 'expo-router'; import { use } from 'react'; import { TabBarContext } from '@/context/TabBarContext'; export default function HomeScreen() { const { setIsTabBarHidden } = use(TabBarContext); useFocusEffect(() => { setIsTabBarHidden(true); return () => setIsTabBarHidden(false); }); return ( // Screen content ); }

有条件地隐藏标签页

如果你想根据条件隐藏标签页,可以移除触发器,或者将 hidden 属性传给 NativeTabs.Trigger 组件。

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { const shouldHideMessagesTab = true; // Replace with your condition return ( <NativeTabs> <NativeTabs.Trigger name="messages" hidden={shouldHideMessagesTab} /> </NativeTabs> ); }

返回行为

默认情况下,点击当前已激活的标签页会关闭该标签页堆栈中的所有屏幕,并返回根屏幕。你可以在 NativeTabs.Trigger 组件上设置 disablePopToTop 属性来禁用此行为。

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index" disablePopToTop> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

滚动到顶部

默认情况下,点击当前已激活且正在显示根屏幕的标签页,会将内容滚动回顶部。你可以在 NativeTabs.Trigger 组件上设置 disableScrollToTop 属性来禁用此行为。

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index" disableScrollToTop> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

禁用标签页

你可以在 NativeTabs.Trigger 组件上设置 disabled 属性,阻止原生选择某个标签页。当该属性为 true 时,点击标签栏中的标签页不会更改当前聚焦的标签页。该标签页仍会保持可见——如果想将其从标签栏中移除,请使用 hidden。

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings" disabled> <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

你也可以在屏幕内部动态切换 disabled。

src/app/checkout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; import { View } from 'react-native'; export default function CheckoutScreen() { const isProcessing = useIsProcessing(); return ( <View> <NativeTabs.Trigger disabled={isProcessing} /> {/* ... */} </View> ); }

iOS 26 功能

独立搜索标签页

要添加独立搜索标签页,请将 role 的值设置为 search,并将其分配给你想要单独显示的原生标签页。

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="search" role="search"> <NativeTabs.Trigger.Label>Search</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

标签栏搜索输入框

要向标签栏添加搜索字段,请将屏幕包裹在 Stack 导航器中,并配置 headerSearchBarOptions。

src
 app
  _layout.tsx
  index.tsx
  search
   _layout.tsx
   index.tsx
src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="search" role="search"> <NativeTabs.Trigger.Label>Search</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }
src/app/search/_layout.tsx
import { Stack } from 'expo-router'; export default function SearchLayout() { return <Stack />; }
src/app/search/index.tsx
import { ScrollView } from 'react-native'; import { Stack } from 'expo-router'; export default function SearchIndex() { return ( <> <Stack.Title>Search</Stack.Title> <Stack.SearchBar placement="automatic" placeholder="Search" onChangeText={() => {}} /> <ScrollView>{/* Screen content */}</ScrollView> </> ); }

标签栏最小化行为

要实现标签栏的最小化行为,可以在 NativeTabs 上使用 minimizeBehavior 属性。以下示例会在向下滚动时最小化标签栏。

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs minimizeBehavior="onScrollDown"> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="tab-1"> <NativeTabs.Trigger.Label>Tab 1</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

底部附加视图

底部附加视图是显示在标签栏上方的浮动视图,适用于显示迷你音乐播放器等持久控件。有关更多详细信息,请参阅 Apple 的 UITabBarController bottomAccessory 文档。

底部附加视图可以显示在两个位置:'regular'(标签栏上方的标准位置)或 'inline'(与标签栏内联的紧凑模式)。使用 usePlacement hook,根据当前位置调整 UI。

以下示例展示了一个由父组件提升状态的迷你播放器:

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; import { useState } from 'react'; import { View, Text, Pressable, StyleSheet } from 'react-native'; function MiniPlayer({ isPlaying, onToggle }) { const placement = NativeTabs.BottomAccessory.usePlacement(); if (placement === 'inline') { // Compact UI for inline placement return ( <Pressable onPress={onToggle} style={styles.inlinePlayer}> <Text>{isPlaying ? '⏸' : '▶'}</Text> </Pressable> ); } // Full UI for regular placement return ( <View style={styles.regularPlayer}> <Text>Now Playing: Song Title</Text> <Pressable onPress={onToggle}> <Text>{isPlaying ? 'Pause' : 'Play'}</Text> </Pressable> </View> ); } export default function TabLayout() { // State must be stored outside BottomAccessory const [isPlaying, setIsPlaying] = useState(false); return ( <NativeTabs> <NativeTabs.BottomAccessory> <MiniPlayer isPlaying={isPlaying} onToggle={() => setIsPlaying(!isPlaying)} /> </NativeTabs.BottomAccessory> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="library"> <NativeTabs.Trigger.Label>Library</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); } const styles = StyleSheet.create({ inlinePlayer: { padding: 8, }, regularPlayer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 16, }, });

Android 上的键盘避让

默认情况下,在 Android 上键盘会覆盖原生标签栏。如果希望标签栏改为移动到键盘上方,请在 NativeTabs 上传入 tabBarRespectsIMEInsets 属性:

app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs tabBarRespectsIMEInsets> <NativeTabs.Trigger name="index" /> <NativeTabs.Trigger name="profile" /> </NativeTabs> ); }

安全区域处理

原生标签页会自动处理安全区域内边距,并具有平台特定的行为:

  • Android:屏幕内容会自动包裹在 SafeAreaView 中,为标签栏应用底部内边距。其他内边距(顶部、左侧、右侧)必须手动处理
  • iOS:原生标签页屏幕中嵌套的第一个 ScrollView 会启用自动内容内边距调整,确保内容能够正确地滚动到标签栏后方

禁用自动内容内边距

如果需要完全控制安全区域处理,可以使用 NativeTabs.Trigger 上的 disableAutomaticContentInsets 属性,禁用自动内容内边距调整:

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index" disableAutomaticContentInsets> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

将 disableAutomaticContentInsets 设置为 true 后,你必须手动管理安全区域内边距。可以使用 react-native-screens/experimental 中的 SafeAreaView:

src/app/index.tsx
import { SafeAreaView } from 'react-native-screens/experimental'; export default function HomeScreen() { return ( <SafeAreaView edges={{ bottom: true }} style={{ flex: 1 }}> {/* Screen content */} </SafeAreaView> ); }

延迟加载

导航器挂载时,原生标签页中的所有标签页屏幕都会立即渲染。由于原生标签栏需要每个屏幕都可用于过渡,因此无法更改此行为。如果某个标签页包含开销较大的内容,而你希望将其延迟到用户实际访问该标签页时再加载,可以使用以下方法之一。

仅在聚焦时渲染内容

使用 useIsFocused 有条件地渲染内容。当用户离开标签页时,内容会卸载;返回时会重新渲染。这意味着每次切换标签页时,任何本地状态(滚动位置、表单输入)都会丢失。

app/(tabs)/search.tsx
import { useIsFocused } from 'expo-router'; import { View, ActivityIndicator, Text } from 'react-native'; export default function SearchScreen() { const isFocused = useIsFocused(); if (!isFocused) { return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <ActivityIndicator /> </View> ); } return ( <View style={{ flex: 1 }}> <Text>Expensive content that only renders when this tab is focused</Text> </View> ); }

首次聚焦时加载一次

使用带有状态标志的 useFocusEffect,在标签页首次聚焦时加载内容,然后保持其挂载状态。

app/(tabs)/search.tsx
import { useFocusEffect } from 'expo-router'; import { useCallback, useState } from 'react'; import { View, ActivityIndicator, Text } from 'react-native'; export default function SearchScreen() { const [hasActivated, setHasActivated] = useState(false); useFocusEffect( useCallback(() => { setHasActivated(true); }, []) ); if (!hasActivated) { return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <ActivityIndicator /> </View> ); } return ( <View style={{ flex: 1 }}> <Text>Content that loads once and stays mounted</Text> </View> ); }

自定义 Web 布局

原生标签页会在 Android 和 iOS 上渲染平台特定的标签栏,但 Web 上没有标准的系统标签栏。在 Web 上,原生标签页会回退到基于 iPad 设计的基本实现。你可以使用 expo-router/ui 中的无头标签页提供自定义 Web 布局,同时在移动设备上保留原生标签页。有两种设置方式。

平台特定的布局文件

在 _layout.tsx 旁边使用 _layout.web.tsx 文件。Web 文件会完全替代 Web 上的布局,因此每个平台都可以使用完全不同的布局。

app
 _layout.tsx — Android 和 iOS 的原生标签页
 _layout.web.tsx — Web 的无头标签页
app/_layout.web.tsx
import { Tabs, TabList, TabTrigger, TabSlot } from 'expo-router/ui'; import { StyleSheet } from 'react-native'; export default function WebLayout() { return ( <Tabs> <TabSlot /> <TabList style={styles.tabList}> <TabTrigger name="index" href="/" style={styles.tab}> Home </TabTrigger> <TabTrigger name="settings" href="/settings" style={styles.tab}> Settings </TabTrigger> </TabList> </Tabs> ); } const styles = StyleSheet.create({ tabList: { flexDirection: 'row', justifyContent: 'center', gap: 16, padding: 16, }, tab: { padding: 8, }, });

使用平台扩展的共享组件

将标签页 UI 提取到带有平台特定扩展的组件中。单个 _layout.tsx 负责处理共享逻辑(providers、包装器、分析),并导入标签页组件,该组件会解析为正确的平台文件。

app
 _layout.tsx — 共享布局,导入 AppTabs
components
 app-tabs.tsx — Android 和 iOS 的原生标签页
 app-tabs.web.tsx — Web 的无头标签页
app/_layout.tsx
import { ThemeProvider, DefaultTheme } from 'expo-router'; import AppTabs from '@/components/app-tabs'; export default function Layout() { return ( <ThemeProvider value={DefaultTheme}> <AppTabs /> </ThemeProvider> ); }
components/app-tabs.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function AppTabs() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon sf="house.fill" md="home" /> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Icon sf="gear" md="settings" /> <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }
components/app-tabs.web.tsx
import { Tabs, TabList, TabTrigger, TabSlot } from 'expo-router/ui'; import { StyleSheet } from 'react-native'; export default function AppTabs() { return ( <Tabs> <TabSlot /> <TabList style={styles.tabList}> <TabTrigger name="index" href="/" style={styles.tab}> Home </TabTrigger> <TabTrigger name="settings" href="/settings" style={styles.tab}> Settings </TabTrigger> </TabList> </Tabs> ); } const styles = StyleSheet.create({ tabList: { flexDirection: 'row', justifyContent: 'center', gap: 16, padding: 16, }, tab: { padding: 8, }, });
自定义标签页

详细了解如何从 expo-router/ui 自定义无头标签页

平台特定的扩展

了解 Expo Router 中 .web.tsx 等平台特定文件扩展名的工作方式

将原生标签页从 SDK 54 迁移到 55

SDK 55 更改了访问标签栏项组件的方式。不再分别导入 Icon、Label 和 Badge,而是使用复合组件 API:NativeTabs.Trigger.Icon、NativeTabs.Trigger.Label 和 NativeTabs.Trigger.Badge。对于 Android 图标,md 属性是使用 Material Symbols 的新推荐方式。

从 JavaScript 标签页迁移

原生标签页并非为JavaScript 标签页的直接替代方案而设计。原生标签页受原生平台行为限制,而 JavaScript 标签页可以更自由地进行自定义。如果你对原生平台行为不感兴趣,可以继续使用 JavaScript 标签页。

使用 Trigger 代替 Screen

NativeTabs 引入了用于向布局添加路由的 Trigger 概念。Screen 会为自动添加的路由设置样式,而 Trigger 系统则让你更好地控制标签页在标签栏中的隐藏和移除。

使用 React 组件代替 props

NativeTabs 采用 React 优先的 API,倾向于使用组件定义 UI,而不是使用 props 对象。

在标签页内使用 Stack

JavaScript <Tabs /> 具有一个原生标签页中不存在的模拟堆栈标题栏。相反,你应在原生标签页内嵌套原生 <Stack /> 布局,以同时支持标题栏和推入屏幕。

常见问题

iOS 18 及更早版本中标签栏是透明的

在 iOS 18 及更早版本中,滚动到可滚动内容末尾时,原生标签栏会变为透明。这意味着,当你滚动到 ScrollView 的末尾或渲染静态 View 时,标签栏会变为透明。

你可以使用 disableTransparentOnScrollEdge 属性禁用此行为。

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index" disableTransparentOnScrollEdge> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

如果你使用 ScrollView 时标签栏从一开始就是透明的,请确保 ScrollView 是屏幕组件的第一个子元素。如果使用其他组件包裹它,请确保将包装器组件的 collapsable 设置为 false。

src/app/index.tsx
import { ScrollView, View } from 'react-native'; export default function HomeScreen() { return ( <View collapsable={false} style={{ flex: 1 }}> <ScrollView>{/* Screen content */}</ScrollView> </View> ); }
在 iOS 26 上切换标签页时出现白色背景闪烁

这是因为默认主题使用白色背景。要修复此问题,请使用适当的主题,将应用包裹在 Expo Router 的 ThemeProvider 中。

支持浅色和深色模式的应用:

src/app/_layout.tsx
import { ThemeProvider, DarkTheme, DefaultTheme } from 'expo-router'; import { NativeTabs } from 'expo-router/native-tabs'; import { useColorScheme } from 'react-native'; export default function TabLayout() { const colorScheme = useColorScheme(); return ( <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> </ThemeProvider> ); }

仅支持深色模式的应用:

src/app/_layout.tsx
import { ThemeProvider, DarkTheme } from 'expo-router'; import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <ThemeProvider value={DarkTheme}> <NativeTabs>{/* tabs */}</NativeTabs> </ThemeProvider> ); }

针对特定背景颜色的替代方案:

如果需要使用与默认主题不匹配的特定背景颜色,可以在 NativeTabs.Trigger 上使用 contentStyle 属性:

<NativeTabs.Trigger name="index" contentStyle={{ backgroundColor: '#1a1a2e' }}>
iOS 26 上标签栏背景属性没有效果

在 iOS 26 及更高版本中,系统会使用 Liquid Glass 绘制标签栏,并根据其后方的内容生成背景。backgroundColor、blurEffect、shadowColor 和 disableTransparentOnScrollEdge 属性仅在 iOS 18 及更早版本中影响 iOS 标签栏。

这也意味着标签栏不会遵循仅存在于 JavaScript 中的配色方案。使用 Appearance.setColorScheme 设置配色方案会改变其他原生视图所解析的界面样式,但不会改变标签栏背景。

要使 iOS 26 上的标签栏与深色 UI 匹配,请按照在 iOS 26 上切换标签页时出现白色背景闪烁中的说明,将标签栏后方的内容设为深色。

点击标签页时无法滚动到顶部

点击当前激活的标签页应将内容滚动到顶部,但如果 ScrollView 不是屏幕组件的第一个子元素,此功能可能无法正常工作。

请确保 ScrollView 是屏幕组件的直接第一个子元素。如果使用其他组件包裹它,请确保将包装器组件的 collapsable 设置为 false。

src/app/index.tsx
import { ScrollView, View } from 'react-native'; export default function HomeScreen() { return ( <View collapsable={false} style={{ flex: 1 }}> <ScrollView>{/* Screen content */}</ScrollView> </View> ); }
iOS 26 深色模式下 Liquid Glass 标题栏按钮闪烁

在 iOS 26 的深色模式下,采用 Liquid Glass 样式的标题栏按钮在切换标签页时可能会闪烁或短暂显示其背景。这是因为默认主题与系统深色模式不匹配,导致 Liquid Glass 渲染出现视觉瑕疵。

修复方式与白色背景闪烁问题相同:使用适当的主题,将布局包裹在来自 expo-router 的 <ThemeProvider> 中。

支持浅色和深色模式的应用:

app/_layout.tsx
import { ThemeProvider, DarkTheme, DefaultTheme } from 'expo-router'; import { NativeTabs } from 'expo-router/native-tabs'; import { useColorScheme } from 'react-native'; export default function TabLayout() { const colorScheme = useColorScheme(); return ( <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> </ThemeProvider> ); }

仅支持深色模式的应用:

app/_layout.tsx
import { ThemeProvider, DarkTheme } from 'expo-router'; import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <ThemeProvider value={DarkTheme}> <NativeTabs>{/* tabs */}</NativeTabs> </ThemeProvider> ); }

已知限制

iOS 上默认图标和选中图标共用一种渲染模式

在 iOS 上,标签页的默认图标和选中图标必须使用相同的渲染模式。当它们解析为不同的模式时,两个图标都会使用默认图标的模式,并且 Expo Router 会在开发环境中记录警告。

当图标颜色仅应用于其中一种状态时,模式会不一致。如果你设置了 tintColor、iconColor={{ selected }},或设置了 Icon 的 selectedColor 属性,却没有同时为默认状态设置颜色,就会发生这种情况。有颜色的图标会使用 'template' 渲染,而无颜色的图标默认为 'original'。仅为一种状态设置 renderingMode 也会产生相同的效果。

要使两个图标以相同方式渲染,请为两种状态都设置颜色,或在 Icon 上设置 renderingMode:

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( // `iconColor` applies to both states, so both icons render as templates <NativeTabs iconColor={{ default: 'gray', selected: 'black' }}> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Icon src={{ default: require('../assets/setting_icon.png'), selected: require('../assets/selected_setting_icon.png'), }} /> </NativeTabs.Trigger> </NativeTabs> ); }

此限制不适用于 SF Symbols,因为系统始终会为其添加色调。

Android 上最多只能包含 5 个标签页

在 Android 上,标签栏最多只能包含 5 个标签页。此限制来自平台的 Material Tabs 组件。

无法测量标签栏高度

标签页会移动位置,例如在 iPad 上渲染时可能位于屏幕顶部,在 Apple Vision Pro 上运行时可能位于屏幕侧面,等等。我们正在开发布局函数,以便未来提供更详细的布局信息。

不支持嵌套原生标签页

原生标签页不能嵌套在其他原生标签页中。你仍然可以在原生标签页中嵌套 JavaScript 标签页。

对 FlatList 的支持有限

FlatList 与原生标签页的集成存在限制。不支持滚动到顶部和滚动时最小化等功能。此外,检测滚动边缘可能会失败,导致标签栏显示为透明。要修复此问题,请使用 disableTransparentOnScrollEdge 属性。

src/app/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs disableTransparentOnScrollEdge> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }
不支持动态添加或移除标签页

不支持在运行时动态添加或移除标签页。标签页应在布局文件中静态定义,并在应用整个生命周期内保持一致。这符合 Apple 人机界面指南中的平台指南,该指南建议保持标签栏内容稳定,以帮助用户建立对应用导航结构的认知模型。如果动态添加或移除标签页,内容将被重新挂载,状态也会丢失。