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 提供了一组组件,可通过子模块 expo-router/ui 创建自定义标签页布局。与 React Navigation 风格化的 Tabs 不同,这些组件没有样式且非常灵活。它们旨在让你在项目中从零开始构建复杂的 UI 模式。

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

原生标签页

如果你想让标签栏具有原生的外观和体验,请查看原生标签页。

JavaScript 标签页

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

自定义 Tabs 组件的结构

expo-router/ui 提供了四个组件来创建自定义标签页布局:

组件描述
Tabs包装组件,包含标签页的 <View>
TabList包含 TabTrigger 组件列表的 <View>
TabTrigger用于切换到指定标签页的触发组件。它通过 href 属性定义路由,并为每个标签页提供一个 name
TabSlot用于渲染当前选中标签页的插槽。

一个最基本的自定义标签页布局结构应由 TabList(包含每个标签页的 TabTrigger 组件)和 TabSlot 组成,且都位于 Tabs 组件内,如下所示:

src/app/(tabs)/_layout.tsx
import { Tabs, TabList, TabTrigger, TabSlot } from 'expo-router/ui'; import { Text } from 'react-native'; // 定义自定义标签导航器的布局 export default function Layout() { return ( <Tabs> <TabSlot /> <TabList> <TabTrigger name="home" href="/"> <Text>主页</Text> </TabTrigger> <TabTrigger name="article" href="/article"> <Text>文章</Text> </TabTrigger> </TabList> </Tabs> ); }

创建路由

TabList 包含标签导航器中可用的所有路由。它必须是 Tabs 的直接子组件。每条路由都由 TabList 内的一个 TabTrigger 定义。TabList 中的 TabTrigger 必须包含 namehref 属性。

通常,TabList 同时定义可用的标签页路由和标签页的外观,而每个 TabTrigger 的子组件则定义每个标签按钮的外观。

动态路由

允许使用动态路由,并且可以通过 href 提供值。

_layout.tsx
[slug].tsx

触发器 <TabTrigger name="dynamic page" href="/hello-world" /> 将为 [slug].tsx 创建一个标签页,参数为 { slug: 'hello-world' }。这种设置对于根据最终用户数据在标签栏中显示任意数量的标签页很有用,例如为应用中的每个用户资料显示一个单独的标签页。

歧义路由

_layout.tsx
(one,two)
route.tsx一个共享组中的路由

传递给 TabTriggerhref 值必须始终指向单一路由。在上面的共享路由示例中,不允许使用 href="/route",因为它可能指向 /(one)/route/(two)/route。不过,在 href 中指定路由组是可以的(例如,href="/(one)/route")。

嵌套路由

_layout.tsx
(stack-one)
_layout.tsx一个 <Stack> 布局
(stack-two)
  _layout.tsx嵌套的 <Stack> 布局
  route.tsx

TabTrigger 可以链接到深层嵌套路由。<TabTrigger name="route" href="/route" /> 将显示 (stack-one)/(stack-two)/route.tsx 路由。此标签页将由该路由的父导航器控制(也就是 stack-two_layout.tsx 中的导航器)。这种导航类似于深层链接。

渲染路由

TabSlot 组件会渲染当前路由。TabSlot 可以嵌套在 Tabs 内的其他组件中,但不能位于 TabList 中。

src/app/_layout.tsx
<Tabs> <TabList> <TabTrigger name="home" href="/"> <Text>主页</Text> </TabTrigger> </TabList> {/* 自定义 `<TabSlot />` 的渲染方式。 */} <View> <View> <TabSlot /> </View> </View> </Tabs>

切换标签页

可以通过 Link 或使用命令式 API 来切换标签页。不过,这些 API 始终会执行导航操作(它们会切换标签页,并且可能更改 URL)。如果你想在不执行任何导航的情况下切换标签页,应使用 TabTriggerTabTrigger 是一个无样式的 <View>,在按下时会切换标签页,类似于将文本和组件包裹在 Link 中以使其成为可点击的导航元素。

重置导航

TabTriggerreset 属性可用于控制标签页何时重置其导航状态。可选值为 alwaysonLongPressnever。这对于嵌套在标签页内的堆栈导航器尤其有用。例如,<TabTrigger name="home" reset="always" /> 会将用户返回到标签页内嵌套堆栈导航器中的索引路由。

TabTrigger

TabTrigger 用于切换标签页,同时也承担着定义哪些路由可作为标签页使用的双重角色。

在 TabList 内

TabTrigger 作为 TabList 的子组件时,它定义了标签导航器中可用的路由。这些 TabTrigger 需要同时包含 namehref 属性,因为它们定义了该标签页的 URL,以及可用于引用该标签页的自定义名称。如果 TabTrigger 组件还包含文本或其他组件作为子元素,那么这些内容也会作为标签按钮渲染出来。不过,你也可以在 TabList 内定义没有任何 UI 的 TabTrigger,然后再由 TabList 外部的 TabTrigger 来触发它们。

在 TabList 外

可以在 TabList 外部定义一个额外的 TabTrigger,从而执行与 TabList 中定义的 TabTrigger 相同的操作。在这种情况下,TabTrigger 不会有 href 属性。相反,它会执行与具有相同 name 属性的主 TabTrigger 相同的操作。这样你就可以创建能够切换标签页、且不依赖当前导航状态的组件。请注意,所有 TabTrigger 至少需要是 Tabs 组件的后代,否则它们会被视为在标签导航器之外,无法触发它。

自定义外观

TabTrigger 会渲染为 <Pressable> 外,所有组件都会以无样式的 <View> 形式渲染。这使你可以通过自定义 style 属性来定制它们的外观。为 TabList 设置样式与在 React Navigation 中自定义标签栏类似,而为 TabTrigger 设置样式则会影响标签按钮的外观。

如果你需要更改某个组件的结构,可以通过使用 asChild 属性来覆盖其底层组件。此时该组件会充当一个插槽,并将其属性转发给紧邻的子组件。

Custom TabList
<Tabs> <TabSlot /> <TabList asChild> {/* 渲染一个自定义的 TabList */} <CustomTabList> <TabTrigger name="home" href="/"> <Text>主页</Text> </TabTrigger> </CustomTabList> </TabList> </Tabs>
Custom Button
<Tabs> <TabSlot /> <TabList asChild> <TabTrigger name="home" href="/" asChild> {/* 渲染一个自定义按钮 */} <CustomButton> <Text>主页</Text> </CustomButton> </TabTrigger> </TabList> </Tabs>

多个标签栏

TabList 既是 Tabs 的配置,也是其默认外观,但它并不是渲染标签栏的唯一方式。通过隐藏 TabList,你可以使用 TabTrigger 构建自定义标签栏。

多个标签栏示例
<Tabs> <TabSlot /> {/* 自定义标签栏 */} <View> <View> <TabTrigger name="home"> <Text>主页</Text> </TabTrigger> <TabTrigger name="article"> <Text>文章</Text> </TabTrigger> </View> </View> <TabList style={{ display: 'none' }}> <TabTrigger name="home" href="/"> <Text>主页</Text> </TabTrigger> <TabTrigger name="article" href="/article"> <Text>文章</Text> </TabTrigger> </TabList> </Tabs>

TabTrigger 会传递一个 isFocused 属性,因此你可以创建一个单独的标签按钮组件来响应聚焦状态。

tab-button.tsx
import FontAwesome from '@expo/vector-icons/FontAwesome'; import { TabTriggerSlotProps } from 'expo-router/ui'; import { ComponentProps, Ref } from 'react'; import { Text, Pressable, View } from 'react-native'; type Icon = ComponentProps<typeof FontAwesome>['name']; export type TabButtonProps = TabTriggerSlotProps & { icon?: Icon; ref: Ref<View>; }; export function TabButton({ icon, children, isFocused, ...props }: TabButtonProps) { return ( <Pressable {...props} style={[ { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexDirection: 'column', gap: 5, padding: 10, }, isFocused ? { backgroundColor: 'white' } : undefined, ]}> <FontAwesome name={icon} /> <Text style={[{ fontSize: 16 }, isFocused ? { color: 'white' } : undefined]}>{children}</Text> </Pressable> ); }
Expo SDK 52 / React 18 及更早版本

在 Expo SDK 52 及更早版本(React 18)中,请使用旧版的 forwardRef 函数来访问 ref 句柄。

Hooks

所有组件也都有对应的 hook 版本,让你可以控制渲染树。有关可用 hooks 的完整列表,请参阅 Router UI 参考

使用 hooks 被认为是该库的高级用法。对于大多数使用场景,使用带有 asChild 的组件应该已经足够让你控制渲染树。

如果你正在开发自定义的 <TabTrigger />,你可能还需要开发自定义的 <TabList />,因为 <TabList /> 使用了 useTabsWithChildren(),而这要求使用导出的 <TabTrigger /> 组件。

自定义标签页屏幕的渲染方式

TabSlot 接受一个 renderFn 属性。该函数可用于覆盖屏幕的渲染方式,使你能够实现动画或屏幕持久化/卸载等高级功能。有关更多信息,请参阅 Router UI 参考

常见问题

如何为同一路由创建多个标签页?
_layout.tsx标签栏布局
(movie,tv)
[id].tsx

你应该将该路由添加到一个共享组中,并为每个组 group 创建一个单独的 TabTrigger

如何隐藏一个标签页?

不渲染 TabTrigger 会从你的应用中移除该标签页(以及它的导航状态)。

如何创建带动画的标签页?

你可以向 TabSlot 提供一个自定义渲染器,以自定义其渲染屏幕的方式。你可以利用它来检测屏幕何时获得焦点,并相应地执行动画。

我可以使用相对 href 吗?
directory
_layout.tsx本地路径名是 /directory
page.tsx路径名是 /directory/page
profile.tsx路径名是 /directory/profile

带有相对 href 的 TabTrigger 是相对于渲染 Tabs 时所在的本地路径名的。这与普通的相对 href 不同,后者是相对于当前显示的路由。例如,<TabTrigger href="./profile" /> 会解析为 /directory/profile,即使当前显示的是 /directory/page 路由。Expo 不建议使用相对 href。