像浏览器一样滚动的 React 可滚动标签栏
一旦你的产品的标签数量超过六个,标签栏就会装不下。解决办法不是缩小字号——而是让整条标签栏可以滚动:溢出交给浏览器处理,点击某个标签会使其居中,箭头只在还有地方可去时才出现。
点击靠近边缘的标签——它会自动滚动到中间位置。
唯一重要的行为:选中即居中
一个可滚动标签栏是否好用,取决于点击边缘标签时会发生什么:它应当平滑滑动到中间,将两侧的相邻标签露出来。这里只需一次调用——scrollToItem(el, 'smooth', 'center')——已经在 选中即居中示例 中接好。挂载时恢复激活标签用的是同一个调用,只是把参数换成 'auto',见 保存与恢复位置。
箭头来自同一份可见性数据:只有当第一个标签移出屏幕时,useLeftArrowVisible 才会为 false,因此左箭头恰好只在有用时才渲染。不需要自己写测量代码,也不需要自己的 resize observer。
如果你正在超出 MUI 可滚动标签栏的能力范围
在 Material 设计体系内,Material UI 的 variant="scrollable" 标签是正确答案——直到你的“标签”不再是标签为止。MUI 把这条栏牢牢焊死在 Tabs 语义上:一对 value/onChange、标签面板,以及 MUI 在移动端默认隐藏的滚动按钮。一旦你的这一行需要容纳纸片、卡片、头像或混合内容,或者需要拖拽滚动,或者需要知道哪些项目可见,你就是在和这个组件较劲,而不是在使用它。
本库处于更底层:一个带可见性跟踪的可滚动行,对“标签”是什么不做任何假设。你的标签可以是任何带有 itemId 的组件——用 Tailwind、MUI 自己的 styled,或纯 CSS 来设置样式都可以。选中状态始终由你掌控,就像上方演示那样,只用一个 useState 保存。
无障碍访问基本是免费的——但要留意两处空白
因为这条栏本身就是一个原生滚动容器,键盘焦点、屏幕阅读器的阅读顺序以及 RTL 都由平台自动处理——焦点在标签间移动时会自动滚动进入可视区域,无需任何代码,RTL 也无需额外配置。有两件事仍需你自己处理,这和其他任何标签 UI 一样:选择合适的 ARIA 模式(如果确实会切换面板,用 role="tablist";如果这些“标签”实际是导航,用 aria-current),并沿用 拖拽滚动 方案中的点击抑制逻辑,以确保松开拖拽时不会误触发某个标签。
最简模式
标签就是带有 itemId 的普通按钮;选中一个会使其居中。这就是全部思路——上方演示只是额外加上了样式和拖拽。
function ScrollTabs({ tabs }: { tabs: string[] }) {
const [active, setActive] = React.useState(tabs[0]);
return (
<ScrollMenu LeftArrow={LeftArrow} RightArrow={RightArrow}>
{tabs.map((tab) => (
<Tab itemId={tab} key={tab} label={tab}
active={tab === active} onSelect={() => setActive(tab)} />
))}
</ScrollMenu>
);
}
function Tab({ itemId, label, active, onSelect }: TabProps) {
const api = React.useContext<publicApiType>(VisibilityContext);
return (
<button
aria-current={active}
onClick={(ev) => {
onSelect();
// The behavior that makes tabs feel native: selecting an
// edge tab glides it to the center of the strip.
api.scrollToItem(ev.currentTarget, 'smooth', 'center');
}}
>
{label}
</button>
);
}或者以 shadcn 组件的形式安装
scroll-tabs 注册表条目以数据驱动的方式提供这一模式——传入 tabs、value、onValueChange 即可——作为可编辑组件安装进你的 components/ui/:
npx shadcn@latest add https://react-horizontal-scrolling-menu.dev/r/scroll-tabs.json