原生选项卡
编辑页面
了解如何在 Expo Router 中使用原生选项卡布局。
For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.

了解如何使用原生选项卡在 iOS 上通过 Expo Router 创建液态玻璃选项卡。
原生选项卡处于 alpha 阶段,可在 SDK 54 及更高版本中使用。其 API 可能会发生变化。
选项卡是应用中不同部分之间导航的常见方式。在 Expo Router 中,你可以根据需要使用不同的选项卡布局。本指南介绍原生选项卡。与 其他选项卡布局 不同,原生选项卡使用原生系统选项卡栏。
其他选项卡布局请参见:
如果你的应用需要完全自定义的设计,而系统选项卡无法实现,请查看自定义选项卡。
如果你已经在使用 React Navigation 的选项卡,请查看 JavaScript 选项卡。
开始使用
你可以使用基于文件的路由来创建选项卡布局。下面是一个示例文件结构:
srcapp_layout.tsxindex.tsxsettings.tsx上面的文件结构会生成一个在屏幕底部带有选项卡栏的布局。该选项卡栏会有两个选项卡:Home 和 Settings。
你可以使用 src/app/_layout.tsx 文件,通过选项卡定义应用的根布局。这个文件是选项卡栏及每个选项卡的主布局文件。在其中,你可以控制选项卡栏和每个选项卡项的外观与行为。
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> ); }
最后,你还需要这两个组成选项卡内容的选项卡文件:src/app/index.tsx 和 src/app/settings.tsx。
import { View, Text, StyleSheet } from 'react-native'; export default function Tab() { return ( <View style={styles.container}> <Text>选项卡 [Home|Settings]</Text> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, });
名为 index.tsx 的选项卡文件是在应用加载时默认显示的选项卡。第二个选项卡文件 settings.tsx 展示了如何向选项卡栏添加更多选项卡。
与 Stack 导航器不同,选项卡不会自动添加到选项卡栏。你需要在布局文件中使用NativeTabs.Trigger显式添加它们。
自定义选项卡栏项
当你想自定义选项卡栏项时,我们建议使用为此设计的组件 API。目前,你可以自定义:
- Icon:显示在选项卡栏项中的图标。
- Label:显示在选项卡栏项中的标签。
- Badge:显示在选项卡栏项中的徽标。
图标
NativeTabs.Trigger.Icon可在 SDK 55 及更高版本中使用。对于 SDK 54,请使用从expo-router/unstable-native-tabs导入的Icon。
你可以使用 Icon 组件来自定义显示在选项卡栏项中的图标。Icon 组件接受 md 属性用于 Android Material Symbols,接受 sf 属性用于 Apple 的 SF Symbols 图标,或接受 src 属性用于自定义图片。
另外,你也可以向 sf 或 src 属性传入 {default: ..., selected: ...},以指定默认状态和选中状态使用不同图标(当前不支持 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 上的 liquid glass 会根据背景颜色是浅色还是深色自动更改颜色。这里没有回调,因此你需要使用 PlatformColor 或 DynamicColorIOS 来设置图标颜色。
import { DynamicColorIOS } from 'react-native'; import { NativeTabs } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs labelStyle={{ // 用于文本颜色 color: DynamicColorIOS({ dark: 'white', light: 'black', }), }} // 用于选中图标颜色 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={{ // 用于文本颜色 color: DynamicColorIOS({ dark: 'white', light: 'black', }), }} // 用于选中图标颜色 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> ); }
图标渲染模式
图标渲染模式可在 SDK 55 及更高版本中使用。
当在 iOS 上使用 src 或 xcasset 属性为自定义图片时,你可以通过 renderingMode 属性控制图标的渲染方式:
template(默认):图标以模板图片形式渲染,允许 iOS 应用色调颜色。这适合需要与应用配色方案一致的单色图标。original:图标会保留原始颜色渲染。这适用于带有渐变或多种颜色的图标。
import { NativeTabs } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> {/* 以原始颜色保留的图标(例如渐变或多色图标) */} <NativeTabs.Trigger name="colorful"> <NativeTabs.Trigger.Icon src={require('../../../assets/colorful_icon.png')} renderingMode="original" /> </NativeTabs.Trigger> {/* 作为模板渲染的图标(默认行为) */} <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> {/* 以原始颜色保留的图标(例如渐变或多色图标) */} <NativeTabs.Trigger name="colorful"> <Icon src={require('../../../assets/colorful_icon.png')} renderingMode="original" /> </NativeTabs.Trigger> {/* 作为模板渲染的图标(默认行为) */} <NativeTabs.Trigger name="simple"> <Icon src={require('../../../assets/simple_icon.png')} renderingMode="template" /> </NativeTabs.Trigger> </NativeTabs> ); }
renderingMode属性只影响 iOS。在 Android 上,所有图片图标都会以原始颜色渲染。
资源目录图标(iOS)
此功能可在 SDK 55 及更高版本中使用。
在 iOS 上,你可以使用 Xcode 资源目录中的图片作为选项卡图标,使用 xcasset 属性即可。这在你希望通过 Xcode 的资源目录管理图标,而不是打包图片文件时非常有用。
传入一个资源名称字符串,可使默认状态和选中状态都使用同一个图标:
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> ); }
若要为默认状态和选中状态使用不同图标,请传入一个对象:
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 组件接受一个作为子元素传入的字符串标签。如果未提供标签,选项卡栏项将使用路由名称作为标签。
如果你不想显示标签,可以使用 hidden 属性将标签隐藏。
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 组件来自定义显示在选项卡栏项上的徽标。徽标是选项卡上方的附加标记,适合用于显示通知或未读消息数量。
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> ); }
自定义选项卡栏
由于原生选项卡布局在不同平台上的外观不同,因此可用的自定义选项也不同。关于所有自定义选项,请参见 NativeTabs 的 API 参考。
高级
隐藏选项卡栏
hidden属性可在 SDK 55 及更高版本中使用。
你可以通过在 NativeTabs 组件上使用 hidden 属性来隐藏选项卡栏。若要为特定屏幕隐藏选项卡栏,你可以使用上下文 API 动态设置 hidden 属性。
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 ( // 屏幕内容 ); }
有条件地隐藏选项卡
动态隐藏选项卡会重新挂载导航器并重置状态。仅应在导航器挂载之前,或在导航器对用户不可见时更改选项卡可见性。
如果你想根据某个条件隐藏选项卡,可以移除 Trigger,或者将 hidden 属性传递给 NativeTabs.Trigger 组件。
import { NativeTabs } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { const shouldHideMessagesTab = true; // 替换为你的条件 return ( <NativeTabs> <NativeTabs.Trigger name="messages" hidden={shouldHideMessagesTab} /> </NativeTabs> ); }
Note:将选项卡标记为hidden表示它无法以任何方式被导航到。
关闭行为
关闭行为可在 SDK 55 及更高版本的 Android 上使用。
默认情况下,点击一个已经处于激活状态的选项卡会关闭该选项卡堆栈中的所有屏幕,并返回根屏幕。你可以通过在 NativeTabs.Trigger 组件上设置 disablePopToTop 属性来禁用此行为。
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> ); }
滚动到顶部
滚动到顶部可在 SDK 55 及更高版本的 Android 上使用。
默认情况下,点击一个已经处于激活状态且显示其根屏幕的选项卡时,内容会滚动回顶部。你可以通过在 NativeTabs.Trigger 组件上设置 disableScrollToTop 属性来禁用此行为。
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 功能
若要使用本节所述功能,请使用 Xcode 26 或更高版本编译你的应用。
独立搜索选项卡
要添加独立的搜索选项卡,请将你想单独显示的原生选项卡的 role 设置为 search。
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> ); }
选项卡栏搜索输入框
要向选项卡栏添加搜索字段,请将屏幕包装在 Stack 导航器中,并配置 headerSearchBarOptions。
srcapp_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>{/* 屏幕内容 */}</ScrollView> </> ); }
选项卡栏最小化行为
要在选项卡栏上实现最小化行为,你可以在 NativeTabs 上使用 minimizeBehavior 属性。在下面的示例中,向下滚动时选项卡栏会最小化。
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> ); }
底部附加视图
此功能可在 SDK 55 及更高版本中使用。
底部附加视图是出现在选项卡栏上方的浮动视图,适合显示持续存在的控件,例如迷你音乐播放器。更多细节请参见 Apple 的 UITabBarController bottomAccessory 文档。
底部附加视图可以出现在两种位置:'regular'(位于选项卡栏上方的标准位置)或 'inline'(紧凑模式,与选项卡栏内联)。请使用 usePlacement 钩子根据当前位置调整 UI。
你必须使用 props、context 或外部状态管理将状态存储在附加组件之外。底部附加组件会同时渲染两个实例(每种位置一个),且它们之间的状态不共享。
下面的示例演示了一个将状态提升到父组件的迷你播放器:
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') { // inline 位置的紧凑 UI return ( <Pressable onPress={onToggle} style={styles.inlinePlayer}> <Text>{isPlaying ? '⏸' : '▶'}</Text> </Pressable> ); } // regular 位置的完整 UI return ( <View style={styles.regularPlayer}> <Text>正在播放:歌曲标题</Text> <Pressable onPress={onToggle}> <Text>{isPlaying ? 'Pause' : 'Play'}</Text> </Pressable> </View> ); } export default function TabLayout() { // 状态必须存储在 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 上显示键盘时,原生选项卡会自动调整以避免被遮挡。你可以在应用配置文件中将 android.softwareKeyboardLayoutMode 属性改为 pan 来禁用此行为:
{ "expo": { "android": { "softwareKeyboardLayoutMode": "pan" } } }
安全区域处理
此功能可在 SDK 55 及更高版本中使用。
原生选项卡会自动处理安全区域边距,并具有平台特定行为:
- Android:屏幕内容会自动包裹在一个
SafeAreaView中,并应用选项卡栏的底部边距。其他边距(顶部、左侧、右侧)必须手动处理。 - iOS:原生选项卡屏幕中嵌套的第一个
ScrollView会启用自动内容边距调整。这可确保内容能在选项卡栏后方正确滚动。
禁用自动内容边距
如果你需要完全控制安全区域处理,可以在 NativeTabs.Trigger 上使用 disableAutomaticContentInsets 属性来禁用自动内容边距调整:
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:
import { SafeAreaView } from 'react-native-screens/experimental'; export default function HomeScreen() { return ( <SafeAreaView edges={{ bottom: true }} style={{ flex: 1 }}> {/* 屏幕内容 */} </SafeAreaView> ); }
懒加载
原生选项卡中的所有选项卡屏幕在导航器挂载时都会立即渲染。由于原生选项卡栏需要每个屏幕都可用于过渡动画,因此这种行为无法更改。如果某个选项卡包含你希望推迟加载的昂贵内容,可以使用以下方法之一。
仅在获得焦点时渲染内容
使用 useIsFocused 有条件地渲染内容。内容会在用户离开时卸载,并在返回时重新渲染。这意味着每次切换选项卡时,任何本地状态(滚动位置、表单输入)都会丢失。
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>仅在该选项卡获得焦点时才渲染的昂贵内容</Text> </View> ); }
首次聚焦时加载一次
使用带有状态标记的 useFocusEffect,在选项卡第一次获得焦点时加载内容,然后保持挂载。
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>只加载一次并保持挂载的内容</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 的无头选项卡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 处理共享逻辑(提供者、包装器、分析),并导入选项卡组件,由其解析到正确的平台文件。
app_layout.tsx — 共享布局,导入 AppTabscomponentsapp-tabs.tsx — Android 和 iOS 的原生选项卡app-tabs.web.tsx — Web 的无头选项卡import 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
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,倾向于使用组件而不是 props 对象来定义 UI。
在选项卡中使用 Stack
JavaScript 的 <Tabs /> 有一个模拟的 stack 头部,而原生选项卡中没有。相反,你应该在原生选项卡内部嵌套一个原生 <Stack /> 布局,以同时支持头部和页面推进。
常见问题
在 iOS 18 及更早版本中,选项卡栏是透明的
在 iOS 18 及更早版本中,当滚动到可滚动内容末尾时,原生选项卡栏会变为透明。这意味着当你滚动到 ScrollView 的末尾,或渲染静态 View 时,它会变成透明。
你可以使用 disableTransparentOnScrollEdge 属性来禁用此行为。
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。
import { ScrollView, View } from 'react-native'; export default function HomeScreen() { return ( <View collapsable={false} style={{ flex: 1 }}> <ScrollView>{/* 屏幕内容 */}</ScrollView> </View> ); }
在 iOS 26 上切换选项卡时白色背景闪烁
这是因为 React Navigation 的默认主题使用了白色背景色。要修复此问题,请使用合适的主题将你的应用包裹在 React Navigation 的 ThemeProvider 中。
适用于同时支持浅色和深色模式的应用:
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 属性:
<NativeTabs.Trigger name="index" contentStyle={{ backgroundColor: '#1a1a2e' }}>
点击选项卡时滚动到顶部不生效
点击当前激活的选项卡应该会将内容滚动到顶部,但如果 ScrollView 不是屏幕组件的第一个子元素,这可能不会生效。
请确保 ScrollView 是屏幕组件的直接第一个子元素。如果你用另一个组件包裹它,请确保在包装组件上将 collapsable 设为 false。
import { ScrollView, View } from 'react-native'; export default function HomeScreen() { return ( <View collapsable={false} style={{ flex: 1 }}> <ScrollView>{/* 屏幕内容 */}</ScrollView> </View> ); }
在 iOS 26 上,深色模式下液态玻璃头部按钮会闪烁
带有液态玻璃样式的头部按钮在 iOS 26 深色模式下切换选项卡时可能会闪烁或背景短暂出现。这是因为 React Navigation 的默认主题与系统深色模式不匹配,导致液态玻璃渲染出现视觉瑕疵。
解决方法与白色背景闪烁问题相同:使用 @react-navigation/native 中的 <ThemeProvider> 并搭配合适的主题包裹你的布局。
适用于同时支持浅色和深色模式的应用:
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> ); }
已知限制
无法测量选项卡栏高度
这些选项卡会移动,有时在 iPad 上渲染时位于屏幕顶部,有时在 Apple Vision Pro 上运行时位于屏幕侧边,等等。我们正在开发一个布局函数,以便未来提供更详细的布局信息。
不支持嵌套原生选项卡
原生选项卡不能嵌套在其他原生选项卡中。不过你仍然可以在原生选项卡中嵌套 JavaScript 选项卡。
对 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> ); }
不支持动态添加或移除选项卡
不支持在运行时动态添加或移除选项卡。选项卡应在布局文件中静态定义,并在整个应用生命周期内保持一致。这符合 Apple 人机界面指南 的平台建议,即保持选项卡栏内容稳定,以帮助用户建立对应用导航结构的心理模型。如果你动态添加或移除选项卡,内容会重新挂载,状态也会丢失。