Sidebar
竖排侧边导航,常与右侧内容区配合
侧边栏(Sidebar)是竖排的分类导航:激活项高亮并在左侧显示一条指示条。它是 TreeSelect 左栏的实现,也可以单独使用。
import { Sidebar } from '@skyroc/native-ui';基础用法
items 声明分类,每项由 key / title 组成。onIndexChange 抛出当前下标,第二个参数直接给出该项配置。
import { Sidebar, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const BASIC_ITEMS = ['推荐', '手机数码', '家用电器', '男装', '女装', '生鲜'].map(title => ({ key: title, title }));
/** 右侧内容区属性 */
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-4">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const SidebarBasic = () => {
const [basicIndex, setBasicIndex] = useState(0);
return (
<View className="bg-background p-4">
<View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
<Sidebar
className="self-stretch"
items={BASIC_ITEMS}
onIndexChange={setBasicIndex}
/>
<Panel
description="指示器落在激活项的垂直中心,切换时做位移动画"
title={BASIC_ITEMS[basicIndex].title}
/>
</View>
</View>
);
};
export { SidebarBasic };根节点默认 self-start(宽度由最长标题撑开)。放进定高的行容器时请自行给宽高,例如 className="w-24 self-stretch"。
何时使用
- 分类多、需要竖排展示的两栏布局(左分类、右内容)。
- 横向的分段切换用
Tabs;两级联动的选择用TreeSelect。
不等高列表
标题换行导致各项高度不一时,指示器仍然逐项对齐 —— 每项各自 onLayout 上报布局,而不是按首项高度推算;指示器自身的高度也是实测的,因此用 classNames.indicator 改高度不会让它错位。
import { Sidebar, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
/** 项高故意不一致:多行标题、单行标题混排,用来验证指示器逐项测量而不是按首项高度推算 */
const UNEVEN_ITEMS = ['热销', '家庭清洁 / 纸品', '个护', '医药健康与营养品', '母婴'].map(title => ({
key: title,
title
}));
/** 右侧内容区属性 */
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-4">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const SidebarUnevenItems = () => {
const [unevenIndex, setUnevenIndex] = useState(3);
return (
<View className="bg-background p-4">
<View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
<Sidebar
className="w-24 self-stretch"
defaultActiveIndex={3}
items={UNEVEN_ITEMS}
onIndexChange={setUnevenIndex}
/>
<Panel
description="试着点最后一项再点第二项,指示器不会越对越偏"
title={UNEVEN_ITEMS[unevenIndex].title}
/>
</View>
</View>
);
};
export { SidebarUnevenItems };角标
badge 传内容、dot 传小红点,内部用 Badge 包裹标题。包裹层是 items-center 收缩到文字宽度的,角标因此贴着标题而不是飞到整项右边缘。
import { Sidebar, Text } from '@skyroc/native-ui';
import type { SidebarItem } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const BADGE_ITEMS: SidebarItem[] = [
{ key: 'all', title: '全部' },
{ badge: 3, key: 'pending', title: '待付款' },
{ key: 'shipping', title: '待收货' },
{ dot: true, key: 'review', title: '待评价' },
{ badge: 128, key: 'refund', title: '退款' }
];
/** 右侧内容区属性 */
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-4">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const SidebarBadge = () => {
const [badgeIndex, setBadgeIndex] = useState(1);
return (
<View className="bg-background p-4">
<View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
<Sidebar
className="self-stretch"
defaultActiveIndex={1}
items={BADGE_ITEMS}
onIndexChange={setBadgeIndex}
/>
<Panel
description="badge 传数字、dot 传小红点,角标贴着标题而不是飞到整项右边缘"
title={BADGE_ITEMS[badgeIndex].title as string}
/>
</View>
</View>
);
};
export { SidebarBadge };自定义标题
title 接受任意节点,可以组合主副文案。字符串标题会自动补上 accessibilityLabel。
import type { SidebarItem } from '@skyroc/native-ui';
import { Sidebar, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const CUSTOM_TITLE_ITEMS: SidebarItem[] = [
{
key: 'new',
title: (
<View className="items-center">
<Text className="text-sm font-medium">新品</Text>
<Text className="text-xs text-primary">NEW</Text>
</View>
)
},
{
key: 'sale',
title: (
<View className="items-center">
<Text className="text-sm font-medium">促销</Text>
<Text className="text-xs text-destructive">SALE</Text>
</View>
)
},
{ key: 'all', title: '全部' }
];
const SidebarCustomTitle = () => {
const [activeKey, setActiveKey] = useState(CUSTOM_TITLE_ITEMS[0].key);
function handleIndexChange(_index: number, item: SidebarItem) {
setActiveKey(item.key);
}
return (
<View className="bg-background p-4">
<View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
<Sidebar
className="self-stretch"
items={CUSTOM_TITLE_ITEMS}
onIndexChange={handleIndexChange}
/>
<View className="flex-1 items-center justify-center gap-2 p-4">
<Text className="text-base font-semibold">当前 key:{activeKey}</Text>
<Text className="text-center text-sm text-muted-foreground">title 可直接传入自定义节点</Text>
</View>
</View>
</View>
);
};
export { SidebarCustomTitle };禁用项
disabled 让该项降到 50% 不透明度、文字转灰且不响应点击。
import { Sidebar, Text } from '@skyroc/native-ui';
import type { SidebarItem } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const DISABLED_ITEMS: SidebarItem[] = [
{ key: 'draft', title: '草稿' },
{ disabled: true, key: 'reviewing', title: '审核中' },
{ key: 'published', title: '已发布' },
{ disabled: true, key: 'archived', title: '已下架' }
];
/** 右侧内容区属性 */
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-4">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const SidebarDisabled = () => {
const [disabledIndex, setDisabledIndex] = useState(0);
return (
<View className="bg-background p-4">
<View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
<Sidebar
className="self-stretch"
items={DISABLED_ITEMS}
onIndexChange={setDisabledIndex}
/>
<Panel
description="禁用项整体降透明度且不响应点击"
title={DISABLED_ITEMS[disabledIndex].title as string}
/>
</View>
</View>
);
};
export { SidebarDisabled };受控
activeIndex + onIndexChange 接管激活项。回调的第二个参数是该项配置,可以按 key 持久化选中态,而不是存一个会串位的下标。
import { Button, Sidebar, Text } from '@skyroc/native-ui';
import type { SidebarItem } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const CONTROLLED_ITEMS = ['基本信息', '收货地址', '支付方式', '发票信息'].map(title => ({ key: title, title }));
/** 右侧内容区属性 */
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-4">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const SidebarControlled = () => {
const [step, setStep] = useState(0);
const [stepKey, setStepKey] = useState(CONTROLLED_ITEMS[0].key);
function changeStep(offset: number) {
const nextStep = Math.min(CONTROLLED_ITEMS.length - 1, Math.max(0, step + offset));
setStep(nextStep);
setStepKey(CONTROLLED_ITEMS[nextStep].key);
}
function handleIndexChange(index: number, item: SidebarItem) {
setStep(index);
setStepKey(item.key);
}
return (
<View className="bg-background p-4">
<View className="mb-4 h-56 flex-row overflow-hidden rounded-xl border border-border/60">
<Sidebar
activeIndex={step}
className="self-stretch"
items={CONTROLLED_ITEMS}
onIndexChange={handleIndexChange}
/>
<Panel
description="激活索引完全由外部 state 决定"
title={CONTROLLED_ITEMS[step].title}
/>
</View>
<View className="flex-row flex-wrap items-center gap-3">
<Button
color="secondary"
variant="outline"
onPress={() => changeStep(-1)}
>
上一项
</Button>
<Button
color="primary"
variant="tonal"
onPress={() => changeStep(1)}
>
下一项
</Button>
<Text className="text-sm text-muted-foreground">
index:{step} / key:{stepKey}
</Text>
</View>
</View>
);
};
export { SidebarControlled };activeIndex 是位置而非身份,items 增删或重排后需要调用方自行校正。
可滚动
scrollable(默认 true)时根节点是 ScrollView,项数超出容器高度可以纵向滚动,指示器跟着内容一起滚。
import { Sidebar, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const SCROLL_ITEMS = Array.from({ length: 20 }, (_, index) => `分类 ${String(index + 1).padStart(2, '0')}`).map(
title => ({ key: title, title })
);
/** 右侧内容区属性 */
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-4">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const SidebarScrollable = () => {
const [scrollIndex, setScrollIndex] = useState(11);
return (
<View className="bg-background p-4">
<View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
<Sidebar
className="self-stretch"
defaultActiveIndex={11}
items={SCROLL_ITEMS}
onIndexChange={setScrollIndex}
/>
<Panel
description="默认激活第 12 项,向下滚动即可看到指示器"
title={SCROLL_ITEMS[scrollIndex].title}
/>
</View>
</View>
);
};
export { SidebarScrollable };不滚动
scrollable={false} 时根节点退化成普通 View,用于外层已经有滚动容器、不希望嵌套滚动的场景。
import { Sidebar, Text } from '@skyroc/native-ui';
import { View } from 'react-native';
const STATIC_ITEMS = ['概览', '明细', '设置'].map(title => ({ key: title, title }));
/** 右侧内容区属性 */
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-4">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const SidebarNotScrollable = () => {
return (
<View className="bg-background p-4">
<View className="flex-row overflow-hidden rounded-xl border border-border/60">
<Sidebar
items={STATIC_ITEMS}
scrollable={false}
/>
<Panel
description="这一块没有固定高度,由侧边栏自身内容撑开"
title="随内容撑开"
/>
</View>
</View>
);
};
export { SidebarNotScrollable };两种模式保持相同的节点层级(root 一层、content 一层),所以 className 与 classNames.content 落在哪个节点上不随 scrollable 变化。
样式覆盖
className 追加到根节点上,classNames 按 slot 细粒度覆盖:
| slot | 作用位置 |
|---|---|
root | 根节点(scrollable 时即 ScrollView 本身) |
content | 内容容器(scrollable 时是 contentContainer) |
item | 单项(激活态换成 bg-background) |
itemText | 单项文字(激活态加粗) |
indicator | 激活项左侧的指示条 |
import { Sidebar, Text } from '@skyroc/native-ui';
import { View } from 'react-native';
const CUSTOM_ITEMS = ['设计', '研发', '测试'].map(title => ({ key: title, title }));
/** 右侧内容区属性 */
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-4">
<Text className="text-base font-semibold">{title}</Text>
<Text className="text-center text-sm text-muted-foreground">{description}</Text>
</View>
);
};
const SidebarCustomSlots = () => {
return (
<View className="bg-background p-4">
<View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
<Sidebar
className="self-stretch bg-muted/40"
classNames={{
content: 'py-2',
indicator: 'h-10 w-1.5 rounded-sm bg-destructive',
item: 'px-6 py-6',
itemText: 'text-base',
root: 'border-r border-destructive/20'
}}
items={CUSTOM_ITEMS}
/>
<Panel
description="root / content / indicator / item / itemText 均可独立覆写"
title="自定义插槽"
/>
</View>
</View>
);
};
export { SidebarCustomSlots };指示器后于各项渲染,靠绘制顺序压在激活项之上 —— zIndex 在 Android 上不总可靠。
无障碍
根节点是 accessibilityRole="tablist",每项是 tab 并带上 selected / disabled 状态;字符串标题会自动成为 accessibilityLabel。
API
Sidebar
除下表外,Sidebar 透传 View 的属性(style、testID 等,children 除外)。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| items* | 侧边栏项数据 | SidebarItem[] | - |
| activeIndex | 受控当前激活索引;索引是位置而非身份,items 变动后需自行校正 | number | - |
| defaultActiveIndex | 非受控初始激活索引 | number | 0 |
| onIndexChange | 激活项变化回调,同时给出该项配置,便于按 key 持久化选中态 | (index: number, item: SidebarItem) => void | - |
| scrollable | 内容超出高度时是否可纵向滚动;false 时根节点退化成普通 View | boolean | true |
| className | 根节点类名,合并在 classNames.root 之后 | string | - |
| classNames | 各 slot 的类名覆盖,见「样式覆盖」一节 | SlotClassNames<SidebarSlots> | - |
类型
import type { SidebarItem, SidebarProps, SidebarSlots } from '@skyroc/native-ui';SidebarSlots
可通过 classNames 覆盖的 slot 名称。
SlotClassNames
classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 SidebarSlots。
SidebarItem
侧边栏单项配置。
| 字段 | 类型 | 说明 |
|---|---|---|
| key* | string | 唯一标识。 |
| title* | ReactNode | 标题;字符串会自动包裹为 Text 并作为无障碍标签。 |
| badge | ReactNode | 徽标内容。 |
| dot | boolean | 是否显示小红点。 |
| disabled | boolean | 是否禁用该项。 |
包内还导出了 sidebarVariants。