React 中的可滚动筛选标签栏
几乎每个搜索栏下方都有这样一行标签——YouTube 的话题、商店筛选项、标签选择器——本质上是一个装满切换按钮的单行滚动容器。难的那 10% 在于边缘处发生的事:新增的标签出现在屏幕外、拖拽绝不能触发任何切换、以及箭头要知道自己何时已经没用了。
添加一个筛选项——该行会自动将新标签滚动进入可视区域。
边缘情况才是这个组件的价值所在
任何带 overflow-x: auto 的 flex 行都能滚动。而一个筛选标签栏的价值恰恰体现在这些细节上:
- 在屏幕外新增的标签必须让自己被看到。 演示中在渲染后会用
apiRef.current.scrollToItem(el, 'smooth', 'end')滚动到每一个新标签——新增项并滚动到该项示例 就是这套接线方式。 - 拖拽用于滚动,点击用于切换——二者绝不能混淆。 桌面用户会像操作触控面板一样拖拽这一行;在某个标签上松开时不能将其翻转。拖拽方案 会跟踪手势,并精确抑制那一次点击。
- 箭头只在有用时出现。
useLeftArrowVisible/useRightArrowVisible接到了与其他一切相同的 IntersectionObserver 上,因此箭头会在真正的边缘处禁用——即便是在标签被新增或移除之后也是如此。
状态始终掌握在你手中
本库负责滚动,不负责选中状态。标签是你自己的按钮——多选切换用 aria-pressed,单选用普通 state——菜单只要求每一个标签携带一个 itemId。这意味着标签状态可以与你已有的任何东西组合:URL 查询参数、表单库,或服务端驱动的筛选模型。删除一个标签就是 移除一个项目;为其加上退场动画则参见 项目动画示例。
移动端:关于页面滚动的一个提醒
在触屏设备上,某些浏览器中在该栏内的横向滑动可能会连带把整个页面也一起拖动。如果你遇到这种情况,阻止页面滚动示例 展示了如何用 touch-action 和 overscroll 约束来锁定这一行为——纯 CSS,不需要任何手势库。
最简模式
标签就是带有 itemId 的切换按钮;通过菜单 API 的 ref 可以将新增的标签滚动进入视图。
function ChipBar({ options }: { options: string[] }) {
const apiRef = React.useRef<publicApiType>(null);
const [selected, setSelected] = React.useState<string[]>([]);
const toggle = (id: string) =>
setSelected((cur) =>
cur.includes(id) ? cur.filter((c) => c !== id) : [...cur, id],
);
// A chip appended off-screen scrolls itself into view.
const addChip = (id: string) => {
toggle(id);
requestAnimationFrame(() => {
const el = apiRef.current?.getItemElementById(id);
if (el) apiRef.current?.scrollToItem(el, 'smooth', 'end');
});
};
return (
<ScrollMenu apiRef={apiRef}>
{options.map((id) => (
<Chip itemId={id} key={id} pressed={selected.includes(id)}
onToggle={() => toggle(id)} />
))}
</ScrollMenu>
);
}或者以 shadcn 组件的形式安装
chip-bar 注册表条目以受控组件的形式提供——options、selected、onSelectedChange——以 Tailwind 样式安装进你的 components/ui/:
npx shadcn@latest add https://react-horizontal-scrolling-menu.dev/r/chip-bar.json