Tabs
标签页,支持手势翻页与面板懒加载
标签页(Tabs)用 items 一次描述标签与面板:上方是可横向滚动的标签栏(指示器随激活项平滑移动),下方是面板区。原生端默认用 react-native-pager-view 提供手势翻页,web 回退到只切 display 的实现。
import { Tabs } from '@skyroc/native-ui';基础用法
items 的每一项由 key / title / children 组成,默认是 line 型(底部一条主题色指示器)。
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { View } from 'react-native';
/** 面板占位内容属性 */
interface PanelProps {
/** 正文说明 */
description: string;
/** 面板标题 */
title: string;
}
const Panel = (props: PanelProps) => {
const { description, title } = props;
return (
<View className="flex-1 items-center justify-center gap-2 p-6">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const ITEMS: TabItem[] = [
{
children: (
<Panel
description="line 型指示器贴在 tabBar 底部,宽度跟随激活项"
title="推荐"
/>
),
key: 'recommend',
title: '推荐'
},
{
children: (
<Panel
description="左右滑动面板即可切换,指示器与滚动位置会跟着动"
title="关注"
/>
),
key: 'following',
title: '关注'
},
{
children: (
<Panel
description="点击 tab 与手势滑动共用同一份激活索引"
title="热榜"
/>
),
key: 'hot',
title: '热榜'
}
];
const TabsBasic = () => {
return (
<View className="bg-background p-4">
<View className="h-56 overflow-hidden rounded-xl border border-border/60">
<Tabs items={ITEMS} />
</View>
</View>
);
};
export { TabsBasic };Tabs 根节点是 flex-1,需要父容器给出确定高度。
何时使用
- 同层级内容的分组切换,且分组数量固定。
- 页面级的顶部标题栏用
NavBar;左侧竖排分类用Sidebar。
Pill 型
type="pill" 把指示器改成撑满 tab 高度的选中背景,标签栏本身是一块圆角灰底,标签平分宽度。
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { View } from 'react-native';
/** 面板占位内容属性 */
interface PanelProps {
/** 正文说明 */
description: string;
/** 面板标题 */
title: string;
}
const Panel = (props: PanelProps) => {
const { description, title } = props;
return (
<View className="flex-1 items-center justify-center gap-2 p-6">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const ITEMS: TabItem[] = [
{
children: (
<Panel
description="pill 型指示器撑满 tab 高度,作为选中态背景"
title="全部"
/>
),
key: 'all',
title: '全部'
},
{
children: (
<Panel
description="tabBar 自身带底色与内边距,tab 等分宽度"
title="进行中"
/>
),
key: 'ongoing',
title: '进行中'
},
{
children: (
<Panel
description="指示器先于文字渲染,靠绘制顺序压在下层"
title="已完成"
/>
),
key: 'done',
title: '已完成'
}
];
const TabsPill = () => {
return (
<View className="bg-background p-4">
<View className="h-56 overflow-hidden rounded-xl border border-border/60">
<Tabs
items={ITEMS}
type="pill"
/>
</View>
</View>
);
};
export { TabsPill };可滚动标签栏
标签超出一屏时标签栏横向滚动,切换后会把激活项滚到视口中间。
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { View } from 'react-native';
/** 面板占位内容属性 */
interface PanelProps {
/** 正文说明 */
description: string;
/** 面板标题 */
title: string;
}
const Panel = (props: PanelProps) => {
const { description, title } = props;
return (
<View className="flex-1 items-center justify-center gap-2 p-6">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const ITEMS: TabItem[] = [
'前端工程',
'客户端',
'人工智能',
'后端开发',
'数据分析',
'音视频',
'安全攻防',
'产品设计'
].map(name => ({
children: (
<Panel
description="tab 总宽超出容器时 tabBar 可横向滚动,激活项会自动居中"
title={name}
/>
),
key: name,
title: name
}));
const TabsScrollable = () => {
return (
<View className="bg-background p-4">
<View className="h-56 overflow-hidden rounded-xl border border-border/60">
<Tabs
defaultActiveIndex={4}
items={ITEMS}
/>
</View>
</View>
);
};
export { TabsScrollable };指示器是先于标签渲染的,靠绘制顺序压在文字下层 —— 负 zIndex 在 Android 上不可靠。
禁用项
items[i].disabled 让该标签不可点,并降到 40% 不透明度。手势滑到禁用页时会沿滑动方向回弹到最近的可用页。
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { View } from 'react-native';
/** 面板占位内容属性 */
interface PanelProps {
/** 正文说明 */
description: string;
/** 面板标题 */
title: string;
}
const Panel = (props: PanelProps) => {
const { description, title } = props;
return (
<View className="flex-1 items-center justify-center gap-2 p-6">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const ITEMS: TabItem[] = [
{
children: (
<Panel
description="从这里向右滑,会越过被禁用的「审核中」"
title="草稿"
/>
),
key: 'draft',
title: '草稿'
},
{
children: (
<Panel
description="不会被展示"
title="审核中"
/>
),
disabled: true,
key: 'reviewing',
title: '审核中'
},
{
children: (
<Panel
description="滑动落到禁用页时,会沿滑动方向回弹到最近的可用页"
title="已发布"
/>
),
key: 'published',
title: '已发布'
},
{
children: (
<Panel
description="不会被展示"
title="已下架"
/>
),
disabled: true,
key: 'archived',
title: '已下架'
}
];
const TabsDisabled = () => {
return (
<View className="bg-background p-4">
<View className="h-56 overflow-hidden rounded-xl border border-border/60">
<Tabs items={ITEMS} />
</View>
</View>
);
};
export { TabsDisabled };受控模式
activeIndex + onIndexChange 把激活项交给外部状态;非受控用 defaultActiveIndex。
import { Button, Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
/** 面板占位内容属性 */
interface PanelProps {
/** 正文说明 */
description: string;
/** 面板标题 */
title: string;
}
const Panel = (props: PanelProps) => {
const { description, title } = props;
return (
<View className="flex-1 items-center justify-center gap-2 p-6">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const ITEMS: TabItem[] = ['第一步', '第二步', '第三步'].map((name, index) => ({
children: (
<Panel
description={`当前是第 ${index + 1} 步,索引完全由外部 state 决定`}
title={name}
/>
),
key: name,
title: name
}));
const TabsControlled = () => {
const [step, setStep] = useState(0);
return (
<View className="gap-4 bg-background p-4">
<View className="h-56 overflow-hidden rounded-xl border border-border/60">
<Tabs
activeIndex={step}
items={ITEMS}
type="pill"
onIndexChange={setStep}
/>
</View>
<View className="flex-row flex-wrap items-center gap-3">
<Button
color="secondary"
variant="outline"
onPress={() => setStep(value => Math.max(0, value - 1))}
>
上一步
</Button>
<Button
color="primary"
variant="tonal"
onPress={() => setStep(value => Math.min(ITEMS.length - 1, value + 1))}
>
下一步
</Button>
<Text className="text-sm text-muted-foreground">activeIndex:{step}</Text>
</View>
</View>
);
};
export { TabsControlled };懒加载
lazy 开启后只渲染当前面板(以及 lazyPreloadDistance 指定的前后各 N 个),其余渲染 renderLazyPlaceholder(默认是一个居中的加载指示器)。
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { useRef } from 'react';
import { View } from 'react-native';
/** 懒加载面板属性 */
interface LazyPanelProps {
/** 面板标题 */
title: string;
}
/** 记录首次挂载时刻,用于验证「加载后常驻、切走不卸载」 */
const LazyPanel = (props: LazyPanelProps) => {
const { title } = props;
const mountedAtRef = useRef(new Date().toLocaleTimeString());
return (
<View className="flex-1 items-center justify-center gap-2 p-6">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-sm text-muted-foreground">挂载于 {mountedAtRef.current}</Text>
<Text className="text-center text-xs text-muted-foreground">来回切换,时间不变说明面板没有被卸载重建</Text>
</View>
);
};
const ITEMS: TabItem[] = ['日报', '周报', '月报', '年报'].map(name => ({
children: <LazyPanel title={name} />,
key: name,
title: name
}));
const TabsLazy = () => {
return (
<View className="bg-background p-4">
<View className="h-56 overflow-hidden rounded-xl border border-border/60">
<Tabs
lazy
items={ITEMS}
lazyPreloadDistance={1}
renderLazyPlaceholder={() => (
<View className="flex-1 items-center justify-center">
<Text className="text-sm text-muted-foreground">尚未加载</Text>
</View>
)}
/>
</View>
</View>
);
};
export { TabsLazy };已加载的面板常驻不卸载(keep-alive 语义):切走只是被外层容器隐藏,面板内部的 state 与滚动位置都会保留。
关闭手势翻页
swipeable={false} 后只能点击标签切换,面板改用「全部挂载、靠 display 切换」的实现。
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { View } from 'react-native';
/** 面板占位内容属性 */
interface PanelProps {
/** 正文说明 */
description: string;
/** 面板标题 */
title: string;
}
const Panel = (props: PanelProps) => {
const { description, title } = props;
return (
<View className="flex-1 items-center justify-center gap-2 p-6">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const ITEMS: TabItem[] = ['概览', '明细'].map(name => ({
children: (
<Panel
description="关闭 swipeable 后只能点击切换,面板改用 display 切换"
title={name}
/>
),
key: name,
title: name
}));
const TabsSwipeDisabled = () => {
return (
<View className="bg-background p-4">
<View className="h-56 overflow-hidden rounded-xl border border-border/60">
<Tabs
items={ITEMS}
swipeable={false}
type="pill"
/>
</View>
</View>
);
};
export { TabsSwipeDisabled };swipeable 会在两棵不同的树之间切换(PagerView ↔ 面板栈),导致面板整体重挂载 —— 请把它当作初始化配置,不要在运行时来回切。web 端没有手势翻页,这个属性在回退实现里是空操作。
样式覆盖
className 追加到根节点上,classNames 按 slot 细粒度覆盖:
| slot | 作用位置 |
|---|---|
root | 根容器 |
tabBar | 标签栏外层(line 型的下边框在这里) |
tabBarContent | 标签的横向容器 |
tab | 单个标签 |
tabText | 标签文字(激活态加粗变主题色) |
indicator | 指示器 |
pager | 面板栈容器(仅非手势实现使用) |
content | 单个面板容器 |
import { Tabs, Text } from '@skyroc/native-ui';
import type { TabItem } from '@skyroc/native-ui';
import { View } from 'react-native';
/** 面板占位内容属性 */
interface PanelProps {
/** 正文说明 */
description: string;
/** 面板标题 */
title: string;
}
const Panel = (props: PanelProps) => {
const { description, title } = props;
return (
<View className="flex-1 items-center justify-center gap-2 p-6">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const ITEMS: TabItem[] = ['设计', '研发', '测试'].map(name => ({
children: (
<Panel
description="通过 classNames 覆写 tabBar / tab / tabText / indicator 各插槽"
title={name}
/>
),
key: name,
title: name
}));
const TabsCustomSlots = () => {
return (
<View className="bg-background p-4">
<View className="h-56 overflow-hidden rounded-xl border border-border/60">
<Tabs
classNames={{
indicator: 'h-1 bg-destructive',
tab: 'px-8 py-4',
tabBar: 'bg-muted/40',
tabText: 'text-base'
}}
items={ITEMS}
/>
</View>
</View>
);
};
export { TabsCustomSlots };title 传节点时不会包 Text,classNames.tabText 也就不再生效,样式自行控制。
API
Tabs
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| items* | Tab 项数据 | TabItem[] | - |
| type | 类型风格 | 'line' | 'pill' | 'line' |
| activeIndex | 受控当前激活索引 | number | - |
| defaultActiveIndex | 非受控初始激活索引 | number | 0 |
| onIndexChange | 激活索引变化回调 | (index: number) => void | - |
| swipeable | 是否开启手势滑动切换;仅原生生效,且应视为初始化配置 | boolean | true |
| lazy | 是否懒加载面板内容;面板一旦加载即常驻,切走不卸载 | boolean | false |
| lazyPreloadDistance | 开启 lazy 时预加载当前 tab 前后 N 个面板 | number | 0 |
| renderLazyPlaceholder | 未加载面板的占位内容,默认是居中的加载指示器 | () => ReactNode | - |
| className | 根容器类名,合并在 classNames.root 之后 | string | - |
| classNames | 各 slot 的类名覆盖,见「样式覆盖」一节 | SlotClassNames<TabsSlots> | - |
| ref | 根节点 View 的 ref,用于 measure / 滚动定位 | Ref<View> | - |
类型
import type { TabItem, TabsProps, TabsSlots, TabsType } from '@skyroc/native-ui';TabsType
标签页风格:line 为底部指示线,pill 为选中背景块。
TabsSlots
可通过 classNames 覆盖的 slot 名称。
SlotClassNames
classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 TabsSlots。
TabItem
单个 tab 的配置。
| 字段 | 类型 | 说明 |
|---|---|---|
| key* | string | 唯一标识。 |
| title* | ReactNode | 标签标题;字符串会自动包裹为 Text。 |
| children | ReactNode | 面板内容。 |
| disabled | boolean | 是否禁用该 tab,手势滑过时会回弹。 |
包内还导出了 tabsVariants。