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

了解如何使用原生标签在 iOS 上通过 Expo Router 创建液态玻璃标签。
重要 Native tabs 处于测试阶段,可在 SDK 54 及更高版本中使用。其 API 可能会发生变化。
标签是在应用的不同部分之间导航的常用方式。在 Expo Router 中,你可以根据需要使用不同的标签布局。本指南介绍原生标签。与其他标签布局不同,原生标签使用的是原生系统的标签栏。
🌐 Tabs are a common way to navigate between different sections of an app. In Expo Router, you can use different tab layouts, depending on your needs. This guide covers the native tabs. Unlike the other tabs layout, native tabs use the native system tab bar.
有关其他选项卡布局,请参见:
🌐 For other tab layouts see:
如果你的应用需要完全自定义的设计,而系统选项卡无法实现,请查看自定义选项卡。
如果你已经在使用 React Navigation 的标签页,请参阅 JavaScript 标签页。
开始使用
🌐 Get started
你可以使用基于文件的路由来创建标签布局。下面是一个示例文件结构:
🌐 You can use file-based routing to create a tabs layout. Here's an example file structure:
app_layout.tsxindex.tsxsettings.tsx上述文件结构会生成一个在屏幕底部带有标签栏的布局。标签栏将有两个标签:首页和设置。
🌐 The above file structure produces a layout with a tab bar at the bottom of the screen. The tab bar will have two tabs: Home and Settings.
你可以使用 app/_layout.tsx 文件来使用标签定义应用的根布局。该文件是标签栏和每个标签的主布局文件。在其中,你可以控制标签栏和每个标签项的外观和行为。
🌐 You can use the app/_layout.tsx file to define your app's root layout using tabs. This file is the main layout file for the tab bar and each tab. Inside it, you can control how the tab bar and each tab item look and behave.
import { NativeTabs } from 'expo-router/unstable-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> ); }
import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <Label>Home</Label> <Icon sf="house.fill" drawable="custom_android_drawable" /> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Icon sf="gear" drawable="custom_settings_drawable" /> <Label>Settings</Label> </NativeTabs.Trigger> </NativeTabs> ); }
最后,你有了构成选项卡内容的两个选项卡文件:app/index.tsx 和 app/settings.tsx。
🌐 Finally, you have the two tab files that make up the content of the tabs: app/index.tsx and 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 展示了如何向标签栏中添加更多标签。
🌐 The tab file named index.tsx is the default tab when the app loads. The second tab file settings.tsx shows how you can add more tabs to the tab bar.
信息 与堆栈导航器不同,选项卡不会自动添加到选项卡栏中。你需要在布局文件中使用
NativeTabs.Trigger明确添加它们。
自定义标签栏项目
🌐 Customizing tab bar items
当你想要自定义选项卡栏项目时,我们建议使用为此目的设计的组件 API。目前,你可以自定义:
🌐 When you want to customize the tab bar item, we recommend using the components API designed for this purpose. Currently, you can customize:
- 图标:在标签栏项目中显示的图标。
- 标签:在标签栏项目中显示的标签。
- 徽章:显示在标签栏项中的徽章。
图标
🌐 Icon
信息
NativeTabs.Trigger.Icon在 SDK 55 及更高版本中可用。对于 SDK 54,请使用从expo-router/unstable-native-tabs导入的Icon。
你可以使用 Icon 组件来自定义标签栏项中显示的图标。Icon 组件接受 md 属性用于 Android Material 图标,sf 属性用于 Apple 的 SF Symbols 图标,或 src 属性用于自定义图片。
🌐 You can use the Icon component to customize the icon displayed in the tab bar item. The Icon component accepts a md prop for Android material symbols, a sf prop for Apple's SF Symbols icons, or a src prop for custom images.
或者,你可以将 {default: ..., selected: ...} 传递给 sf 或 src 属性,以指定默认状态和选中状态的不同图标(目前在 Android 上不支持)。
🌐 Alternatively, you can pass {default: ..., selected: ...} to either the sf or src prop to specify different icons for the default and selected states (currently not supported on Android).
import { NativeTabs } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <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={require('../../../assets/setting_icon.png')} /> </NativeTabs.Trigger> </NativeTabs> ); }
import { NativeTabs, Icon } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <Icon sf={{ default: 'house', selected: 'house.fill' }} drawable="custom_home_drawable" /> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Icon src={require('../../../assets/setting_icon.png')} /> </NativeTabs.Trigger> </NativeTabs> ); }
iOS 上的液态玻璃会根据背景颜色是浅色还是深色自动更改颜色。没有回调,因此你需要使用 PlatformColor 或 DynamicColorIOS 来设置图标的颜色。
🌐 Liquid glass on iOS automatically changes colors based on if the background color is light or dark. There is no callback for this, so you need to use a PlatformColor or DynamicColorIOS to set the color of the icon.
import { DynamicColorIOS } from 'react-native'; import { NativeTabs } from 'expo-router/unstable-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> ); }
import { DynamicColorIOS } from 'react-native'; import { NativeTabs, Icon } from 'expo-router/unstable-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"> <Icon sf={{ default: 'house', selected: 'house.fill' }} drawable="custom_home_drawable" /> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Icon src={{ default: require('../assets/setting_icon.png'), selected: require('../assets/selected_setting_icon.png'), }} /> </NativeTabs.Trigger> </NativeTabs> ); }
图标渲染模式
🌐 Icon rendering mode
重要 图标渲染模式在 SDK 55 及更高版本中可用。
在 iOS 上使用 src 或 xcasset 属性自定义图片时,你可以通过 renderingMode 属性控制图标的渲染方式:
🌐 When using the src or xcasset prop for custom images on iOS, you can control how the icon is rendered with the renderingMode prop:
template(默认):图标将呈现为模板图片,允许 iOS 应用色彩覆盖。这非常适合需要与应用配色方案匹配的单色图标。original:图标以保留其原始颜色的方式呈现。这对于具有渐变或多种颜色的图标非常有用。
import { NativeTabs } from 'expo-router/unstable-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> ); }
import { NativeTabs, Icon } from 'expo-router/unstable-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"> <Icon src={require('../../../assets/colorful_icon.png')} renderingMode="original" /> </NativeTabs.Trigger> {/* Icon rendered as a template (default behavior) */} <NativeTabs.Trigger name="simple"> <Icon src={require('../../../assets/simple_icon.png')} renderingMode="template" /> </NativeTabs.Trigger> </NativeTabs> ); }
信息
renderingMode属性仅影响 iOS。在 Android 上,所有图片图标都将以其原始颜色呈现。
资源目录图标(iOS)
🌐 Asset catalog icons (iOS)
信息 此功能在 SDK 55 及更高版本中可用。
在 iOS 上,你可以使用来自 Xcode 资源目录的图片作为标签图标,通过 xcasset 属性实现。当你想通过 Xcode 的资源目录来管理图标,而不是打包图片文件时,这非常有用。
🌐 On iOS, you can use images from the Xcode asset catalog as tab icons with the xcasset prop. This is useful when you want to manage your icons through Xcode's asset catalog instead of bundling image files.
传递一个包含资源名称的字符串,以便在默认状态和选中状态下使用相同的图标:
🌐 Pass a string with the asset name to use the same icon for both default and selected states:
import { NativeTabs } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Icon xcasset="home-icon" /> </NativeTabs.Trigger> </NativeTabs> ); }
要为默认状态和选中状态使用不同的图标,请传入一个对象:
🌐 To use different icons for default and selected states, pass an object:
import { NativeTabs } from 'expo-router/unstable-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> ); }
信息 资源目录图标支持
renderingMode属性,就像src图标一样。当设置iconColor时,图标默认使用template渲染。否则,它们默认使用original。
标签
🌐 Label
你可以使用 Label 组件来自定义在标签栏项目中显示的标签。Label 组件接受作为子元素传入的字符串标签。如果没有提供标签,标签栏项目将使用路由名称作为标签。
🌐 You can use the Label component to customize the label displayed in the tab bar item. The Label component accepts a string label passed as a child. If no label is provided, the tab bar item will use the route name as the label.
如果你不想显示标签,可以使用 hidden 属性来隐藏标签。
🌐 If you don't want to display a label, you can use the hidden prop to hide the label.
import { NativeTabs } from 'expo-router/unstable-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> ); }
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <Label>Home</Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Label hidden /> </NativeTabs.Trigger> </NativeTabs> ); }
徽章
🌐 Badge
你可以使用 Badge 组件来自定义标签栏项目显示的徽章。徽章是在标签顶部的一个附加标记,可用于显示通知或未读消息数量。
🌐 You can use the Badge component to customize the badge displayed for the tab bar item. The badge is an additional mark on top of the tab and useful for showing notification or unread message counts.
import { NativeTabs } from 'expo-router/unstable-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> ); }
import { NativeTabs, Badge } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="messages"> <Badge>9+</Badge> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Badge /> </NativeTabs.Trigger> </NativeTabs> ); }
自定义标签栏
🌐 Customizing the tab bar
由于原生标签布局的外观因平台而异,自定义选项也不同。有关所有自定义选项,请参阅 NativeTabs 的 API 参考。
🌐 Since the native tab layout's appearance varies by platform, the customization options are also different. For all customization options, see the API reference for NativeTabs.
高级
🌐 Advanced
隐藏标签栏
🌐 Hiding the Tab bar
信息
hidden属性在 SDK 55 及更高版本可用。
你可以在 NativeTabs 组件上使用 hidden 属性来隐藏标签栏。要在特定屏幕隐藏标签栏,你可以使用上下文 API 动态设置 hidden 属性。
🌐 You can hide the tab bar using hidden prop on the NativeTabs component. To hide tab bar for specific screens, you can use context API to set the hidden prop dynamically.
import { createContext } from 'react'; export const TabBarContext = createContext<{ setIsTabBarHidden: (hidden: boolean) => void; }>({ setIsTabBarHidden: () => {}, });
import { NativeTabs } from 'expo-router/unstable-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> ); }
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 ); }
有条件地隐藏标签页
🌐 Hiding a tab conditionally
警告 动态隐藏标签页会重新挂载导航器,并且状态将被重置。仅在导航器挂载之前或者导航器对用户不可见时更改标签页的可见性。
如果你想根据某个条件隐藏标签页,你可以选择移除触发器或者将 hidden 属性传递给 NativeTabs.Trigger 组件。
🌐 If you want to hide a tab based on a condition, you can either remove the trigger or pass the hidden prop to the NativeTabs.Trigger component.
import { NativeTabs } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { const shouldHideMessagesTab = true; // Replace with your condition return ( <NativeTabs> <NativeTabs.Trigger name="messages" hidden={shouldHideMessagesTab} /> </NativeTabs> ); }
信息 注意:将标签页标记为
hidden意味着无法以任何方式导航到该标签页。
关闭行为
🌐 Dismiss behavior
信息 在 Android 上,SDK 55 及更高版本可用的解除行为。
默认情况下,点击已经处于活动状态的标签会关闭该标签堆栈中的所有屏幕并返回到根屏幕。你可以通过在 NativeTabs.Trigger 组件上设置 disablePopToTop 属性来禁用此功能。
🌐 By default, tapping a tab that is already active closes all screens in that tab's stack and returns to the root screen. You can disable this by setting the disablePopToTop prop on the NativeTabs.Trigger component.
import { NativeTabs } from 'expo-router/unstable-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> ); }
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index" disablePopToTop> <Label>Home</Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Label>Settings</Label> </NativeTabs.Trigger> </NativeTabs> ); }
滚动到顶部
🌐 Scroll to top
信息 在 Android 上,滚动到顶部功能在 SDK 55 及更高版本可用。
默认情况下,点击一个已经处于活动状态并显示其根屏幕的标签时,会将内容滚动回顶部。你可以通过在 NativeTabs.Trigger 组件上设置 disableScrollToTop 属性来禁用此功能。
🌐 By default, tapping a tab that is already active and showing its root screen scrolls the content back to the top. You can disable this by setting the disableScrollToTop prop on the NativeTabs.Trigger component.
import { NativeTabs } from 'expo-router/unstable-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> ); }
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index" disableScrollToTop> <Label>Home</Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Label>Settings</Label> </NativeTabs.Trigger> </NativeTabs> ); }
iOS 26 功能
🌐 iOS 26 features
信息 要使用本节中描述的功能,请使用 Xcode 26 或更高版本编译你的应用。
独立搜索标签
🌐 Separate search tab
要添加一个单独的搜索标签,将 role 设置为 search 的值,并分配给你想单独显示的原生标签。
🌐 To add a separate search tab, assign the role with its value set to search to the native tab you want to display separately.
import { NativeTabs } from 'expo-router/unstable-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> ); }
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <Label>Home</Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="search" role="search"> <Label>Search</Label> </NativeTabs.Trigger> </NativeTabs> ); }
标签栏搜索输入
🌐 Tabbar search input
要在标签栏中添加搜索字段,请将屏幕封装在 Stack 导航器中并配置 headerSearchBarOptions。
🌐 To add a search field to the tab bar, wrap the screen in a Stack navigator and configure headerSearchBarOptions.
app_layout.tsxindex.tsxsearch_layout.tsxindex.tsximport { NativeTabs } from 'expo-router/unstable-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> ); }
import { Stack } from 'expo-router'; export default function SearchLayout() { return <Stack />; }
import { ScrollView } from 'react-native'; import { Stack } from 'expo-router'; export default function SearchIndex() { return ( <> <Stack.Screen.Title>Search</Stack.Screen.Title> <Stack.SearchBar placement="automatic" placeholder="Search" onChangeText={() => {}} /> <ScrollView>{/* Screen content */}</ScrollView> </> ); }
标签栏最小化行为
🌐 Tab bar minimize behavior
要在标签栏上实现最小化行为,你可以在NativeTabs上使用minimizeBehavior 属性。下面的示例中,当向下滚动时,标签栏会最小化。
🌐 To implement the minimized behavior on the tab bar, you can use
minimizeBehavior prop on
NativeTabs. In the example below, the tab bar is minimized when scrolling down.
import { NativeTabs } from 'expo-router/unstable-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> ); }
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs minimizeBehavior="onScrollDown"> <NativeTabs.Trigger name="index"> <Label>Home</Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="tab-1"> <Label>Tab 1</Label> </NativeTabs.Trigger> </NativeTabs> ); }
底部配件
🌐 Bottom accessory
信息 此功能在 SDK 55 及更高版本中可用。
底部附加组件是一种浮动视图,出现在标签栏上方,可用于显示持久控件,例如迷你音乐播放器。更多详情请参见 Apple 的 UITabBarController bottomAccessory 文档。
🌐 A bottom accessory is a floating view that appears above the tab bar, useful for displaying persistent controls like a mini music player. See Apple's UITabBarController bottomAccessory documentation for more details.
底部配件可以出现在两种位置:'regular'(位于标签栏上方的标准位置)或 'inline'(紧凑模式,与标签栏在同一行)。使用 usePlacement 钩子根据当前的位置调整你的用户界面。
🌐 The bottom accessory can appear in two placements: 'regular' (standard position above the tab bar) or 'inline' (compact mode, inline with the tab bar). Use the usePlacement hook to adapt your UI based on the current placement.
警告 你必须使用 props、context 或外部状态管理在配件组件之外存储状态。底部配件组件的两个实例会同时渲染(每个位置各一个),它们之间的状态 不会 共享。
以下示例演示了一个将状态提升到父组件的迷你播放器:
🌐 The following example demonstrates a mini player with state lifted to the parent component:
import { NativeTabs } from 'expo-router/unstable-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 上禁用键盘避让
🌐 Disabling keyboard avoidance on Android
默认情况下,当Android上显示键盘时,本地标签会自动调整,以避免被遮挡。你可以通过在应用配置文件中将 android.softwareKeyboardLayoutMode 属性更改为 pan 来禁用此行为:
🌐 By default, when the keyboard is displayed on Android, the native tabs automatically adjust to avoid being obscured. You can disable this behavior by changing the android.softwareKeyboardLayoutMode property to pan in your app config file:
{ "expo": { "android": { "softwareKeyboardLayoutMode": "pan" } } }
安全区域处理
🌐 Safe area handling
信息 此功能在 SDK 55 及更高版本中可用。
原生标签会自动处理安全区域插入,并具有特定平台的行为:
🌐 Native tabs automatically handle safe area insets, with platform-specific behavior:
- Android:屏幕内容会自动封装在一个
SafeAreaView中,该控件会为标签栏应用 底部 内边距。其他内边距(顶部、左侧、右侧)必须手动处理。 - iOS:在原生标签屏幕内嵌套的第一个
ScrollView开启了自动内容插入调整。这确保了内容在标签栏后正确滚动。
禁用自动内容插入
🌐 Disabling automatic content insets
如果你需要完全控制安全区域的处理,可以在 NativeTabs.Trigger 上使用 disableAutomaticContentInsets 属性来禁用自动内容插入调整:
🌐 If you need full control over safe area handling, you can disable automatic content inset adjustment using the disableAutomaticContentInsets prop on NativeTabs.Trigger:
import { NativeTabs } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index" disableAutomaticContentInsets> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index" disableAutomaticContentInsets> <Label>Home</Label> </NativeTabs.Trigger> </NativeTabs> ); }
当 disableAutomaticContentInsets 设置为 true 时,你必须手动管理安全区域内边距。你可以使用来自 react-native-screens/experimental 的 SafeAreaView:
🌐 When disableAutomaticContentInsets is set to true, you must manage safe area insets manually. You can use SafeAreaView from react-native-screens/experimental:
import { SafeAreaView } from 'react-native-screens/experimental'; export default function HomeScreen() { return ( <SafeAreaView edges={{ bottom: true }} style={{ flex: 1 }}> {/* Screen content */} </SafeAreaView> ); }
延迟加载
🌐 Lazy loading
当导航器加载时,原生标签中的所有标签屏幕都会被立即渲染。此行为无法更改,因为原生标签栏需要每个屏幕都可用于过渡。如果某个标签包含开销较大的内容,且你希望延迟到用户实际访问该标签时再加载,可以使用以下方法之一。
🌐 All tab screens in native tabs render eagerly when the navigator mounts. This behavior cannot be changed because the native tab bar needs each screen to be available for transitions. If a tab contains expensive content that you want to defer until the user actually visits the tab, you can use one of the following approaches.
仅在聚焦时渲染内容
🌐 Render content only when focused
使用 useIsFocused 有条件地渲染内容。当用户离开时,内容会被卸载,返回时会重新渲染。这意味着每次切换标签时,任何本地状态(滚动位置、表单输入)都会丢失。
🌐 Use useIsFocused to conditionally render content. The content unmounts when the user navigates away and re-renders when they come back. This means any local state (scroll position, form inputs) is lost on every tab switch.
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> ); }
首次聚焦时加载一次
🌐 Load once on first focus
使用 useFocusEffect 和状态标志在标签页第一次被聚焦时加载内容,然后保持其挂载状态。
🌐 Use useFocusEffect with a state flag to load content the first time the tab is focused, then keep it mounted.
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> ); }
自定义网页布局
🌐 Custom web layout
原生标签在 Android 和 iOS 上呈现平台特定的标签栏,但在网页上没有标准的系统标签栏。在网页上,原生标签会回退到一个基于 iPad 设计的基本实现。你可以使用 expo-router/ui 的无头标签来提供自定义网页布局,同时在移动端保持原生标签。有两种方法可以设置此功能。
🌐 Native tabs render platform-specific tab bars on Android and iOS, but there is no standard system tab bar on web. On web, native tabs fall back to a basic implementation, loosely based on iPad design. You can use headless tabs from expo-router/ui to provide a custom web layout while keeping native tabs on mobile. There are two ways to set this up.
特定平台的布局文件
🌐 Platform-specific layout files
在你的 _layout.tsx 文件旁边使用一个 _layout.web.tsx 文件。Web 文件会完全替换 web 端的布局,因此每个平台可以有完全不同的布局。
🌐 Use a _layout.web.tsx file alongside your _layout.tsx. The web file completely replaces the layout on web, so each platform can have an entirely different layout.
app_layout.tsx — native tabs for Android and iOS_layout.web.tsx — headless tabs for webimport { 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, }, });
带有平台扩展的共享组件
🌐 Shared component with platform extensions
将标签页 UI 提取到具有平台特定扩展的组件中。单个 _layout.tsx 处理共享逻辑(提供者、封装器、分析),并导入标签页组件,该组件会解析为正确的平台文件。
🌐 Extract the tab UI into a component with platform-specific extensions. A single _layout.tsx handles shared logic (providers, wrappers, analytics) and imports the tab component, which resolves to the correct platform file.
app_layout.tsx — shared layout, imports AppTabscomponentsapp-tabs.tsx — native tabs for Android and iOSapp-tabs.web.tsx — headless tabs for webimport AppTabs from '@/components/app-tabs'; export default function Layout() { return ( <ThemeProvider> <AppTabs /> </ThemeProvider> ); }
import { NativeTabs } from 'expo-router/unstable-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> ); }
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 自定义无头标签的信息。
了解像 .web.tsx 这样的特定平台文件扩展名在 Expo Router 中如何工作。
将原生标签从 SDK 54 迁移到 55
🌐 Migrating native tabs from SDK 54 to 55
SDK 55 改变了访问标签栏项目组件的方式。你不再需要分别导入 Icon、Label 和 Badge,而是使用复合组件 API:NativeTabs.Trigger.Icon、NativeTabs.Trigger.Label 和 NativeTabs.Trigger.Badge。对于 Android 图标,md 属性是使用 Material Symbols 的新推荐方式。
🌐 SDK 55 changes how you access tab bar item components. Instead of importing Icon, Label, and Badge separately, use the compound component API: NativeTabs.Trigger.Icon, NativeTabs.Trigger.Label, and NativeTabs.Trigger.Badge. For Android icons, the md prop is the new recommended way to use Material Symbols.
从 JavaScript 标签页迁移
🌐 Migrating from JavaScript tabs
原生标签并不是用来替代JavaScript 标签的。原生标签受限于原生平台的行为,而 JavaScript 标签可以更自由地定制。如果你对原生平台的行为不感兴趣,可以继续使用 JavaScript 标签。
🌐 Native tabs are not designed to be a drop-in replacement for JavaScript tabs. The native tabs are constrained to the native platform behavior, whereas the JavaScript tabs can be customized more freely. If you aren't interested in the native platform behavior, you can continue using the JavaScript tabs.
使用 Trigger 替代 Screen
🌐 Use Trigger instead of Screen
NativeTabs 引入了 Trigger 的概念,用于向布局中添加路由。与自动添加路由并进行样式处理的 Screen 不同,Trigger 系统可以更好地控制标签栏中的标签隐藏和移除。
使用 React 组件代替 props
🌐 Use React components instead of props
NativeTabs 有一个以 React 为首的 API,选择使用组件来定义 UI,而不是使用 props 对象。
在标签页中使用 Stacks
🌐 Use Stacks inside tabs
JavaScript 的 <Tabs /> 有一个模拟的栈头,而原生标签页中没有。相反,你应该在原生标签页内嵌套一个原生 <Stack /> 布局,以支持头部和推送屏幕。
🌐 The JavaScript <Tabs /> have a mock stack header which is not present in the native tabs. Instead, you should nest a native <Stack /> layout inside the native tabs to support both headers and pushing screens.
常见问题
🌐 Common problems
在 iOS 18 及更早版本上,标签栏是透明的
在 iOS 18 及更早版本中,当滚动到可滚动内容的末尾时,原生标签栏会变得透明。这意味着当你滚动到 ScrollView 的末尾或渲染静态 View 时,它会变得透明。
🌐 On iOS 18 and earlier, the native tab bar becomes transparent when scrolling to the end of a scrollable content. This means that it will become transparent when you scroll to the end of a ScrollView or when you render a static View.
你可以使用 disableTransparentOnScrollEdge 属性来禁用此行为。
🌐 You can use the disableTransparentOnScrollEdge prop to disable this behavior.
import { NativeTabs } from 'expo-router/unstable-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。
🌐 When you are using a ScrollView and the tab bar is transparent from the start, ensure that the ScrollView is a first child of the screen component. If you wrap it with another component make sure to set collapsable to false on the wrapper component.
import { ScrollView, View } from 'react-native'; export default function HomeScreen() { return ( <View collapsable={false} style={{ flex: 1 }}> <ScrollView>{/* Screen content */}</ScrollView> </View> ); }
在 iOS 26 上切换标签时白色背景闪烁
这是因为 React Navigation 的默认主题使用了白色背景。要解决此问题,请使用适当的主题将你的应用封装在 React Navigation 的 ThemeProvider 中。
🌐 This happens because React Navigation's default theme uses a white background color. To fix this, wrap your app in React Navigation's ThemeProvider with the appropriate theme.
对于同时支持浅色和夜间模式的应用:
import { ThemeProvider, DarkTheme, DefaultTheme } from '@react-navigation/native'; import { NativeTabs } from 'expo-router/unstable-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> ); }
仅适用于夜间模式应用:
import { ThemeProvider, DarkTheme } from '@react-navigation/native'; import { NativeTabs } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <ThemeProvider value={DarkTheme}> <NativeTabs>{/* tabs */}</NativeTabs> </ThemeProvider> ); }
特定背景颜色的替代方案:
如果你需要一个与默认主题不匹配的特定背景颜色,你可以在 NativeTabs.Trigger 上使用 contentStyle 属性:
🌐 If you need a specific background color that doesn't match the default themes, you can use the contentStyle prop on NativeTabs.Trigger:
<NativeTabs.Trigger name="index" contentStyle={{ backgroundColor: '#1a1a2e' }}>
点击标签时滚动到顶部无法使用
点击一个活动标签页应该会将内容滚动到顶部,但如果 ScrollView 不是屏幕组件的第一个子元素,这可能不起作用。
🌐 Tapping an active tab should scroll the content to the top, but this may not work if the ScrollView is not the first child of the screen component.
确保 ScrollView 是屏幕组件的直接第一个子元素。如果你将它封装在另一个组件中,确保在封装组件上将 collapsable 设置为 false。
🌐 Ensure that the ScrollView is a direct first child of the screen component. If you wrap it with another component, make sure to set collapsable to false on the wrapper component.
import { ScrollView, View } from 'react-native'; export default function HomeScreen() { return ( <View collapsable={false} style={{ flex: 1 }}> <ScrollView>{/* Screen content */}</ScrollView> </View> ); }
在 iOS 26 的暗黑模式下,液态玻璃标题按钮会闪烁
带有液态玻璃样式的标题按钮在 iOS 26 的夜间模式下切换标签时,背景可能会闪烁或闪现。这是因为 React Navigation 的默认主题与系统夜间模式不匹配,导致液态玻璃渲染出现视觉异常。
🌐 Header buttons with liquid glass styling may flicker or flash their background when switching tabs in dark mode on iOS 26. This happens because React Navigation's default theme doesn't match the system dark mode, causing visual artifacts in the liquid glass rendering.
解决方法与白色背景闪烁问题相同:使用适当的主题,用来自 @react-navigation/native 的 <ThemeProvider> 封装你的布局。
🌐 The fix is the same as for the white background flash issue: wrap your layout with <ThemeProvider> from @react-navigation/native using the appropriate theme.
对于同时支持浅色和夜间模式的应用:
import { ThemeProvider, DarkTheme, DefaultTheme } from '@react-navigation/native'; import { NativeTabs } from 'expo-router/unstable-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> ); }
仅适用于夜间模式应用:
import { ThemeProvider, DarkTheme } from '@react-navigation/native'; import { NativeTabs } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <ThemeProvider value={DarkTheme}> <NativeTabs>{/* tabs */}</NativeTabs> </ThemeProvider> ); }
已知的限制
🌐 Known limitations
Android 上最多 5 个标签页
在 Android 上,标签栏最多只能有 5 个标签。这一限制来自平台的 Material Tabs 组件。
🌐 On Android, there is a limitation of having a maximum of 5 tabs in the tab bar. This restriction comes from the platform's Material Tabs component.
无法测量标签栏高度
标签会移动,有时在 iPad 上渲染时显示在屏幕顶部,有时在 Apple Vision Pro 上运行时显示在屏幕一侧,等等。我们正在开发一个布局功能,以便将来提供更详细的布局信息。
🌐 The tabs move around, sometimes being on top of the screen when rendering on iPad, sometimes on the side of the screen when running on Apple Vision Pro, and so on. We're working on a layout function to provide more detailed layout info in the future.
不支持嵌套的原生标签
原生标签不能嵌套在其他原生标签内。但你仍然可以在原生标签内嵌套JavaScript 标签。
🌐 Native tabs cannot be nested inside other native tabs. You can still nest JavaScript tabs inside native tabs.
对 FlatList 的支持有限
FlatList 与原生标签页的集成存在限制。像“滚动到顶部”和“滚动时最小化”这样的功能不受支持。此外,检测滚动边缘可能会失败,导致标签栏显示为透明。要解决此问题,请使用 disableTransparentOnScrollEdge 属性。
import { NativeTabs } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs disableTransparentOnScrollEdge> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }
import { NativeTabs, Label } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs disableTransparentOnScrollEdge> <NativeTabs.Trigger name="index"> <Label>Home</Label> </NativeTabs.Trigger> </NativeTabs> ); }
不支持动态添加或移除标签页
在运行时动态添加或移除选项卡是不支持的。选项卡应在布局文件中静态定义,并在应用的整个生命周期中保持一致。这与苹果人机界面指南中的平台指南一致,该指南建议保持标签栏内容稳定,以帮助用户建立应用导航结构的心理模型。如果动态添加或移除选项卡,内容将被重新挂载,状态将丢失。
🌐 Dynamically adding or removing tabs at runtime is not supported. Tabs should be defined statically in your layout file and remain consistent throughout the app's lifecycle. This aligns with platform guidelines from Apple's Human Interface Guidelines which recommend keeping tab bar content stable to help users build a mental model of your app's navigation structure. If you dynamically add or remove tabs, the content will be remounted and the state will be lost.