React 中的可滚动筛选标签栏

几乎每个搜索栏下方都有这样一行标签——YouTube 的话题、商店筛选项、标签选择器——本质上是一个装满切换按钮的单行滚动容器。难的那 10% 在于边缘处发生的事:新增的标签出现在屏幕外、拖拽绝不能触发任何切换、以及箭头要知道自己何时已经没用了。

添加一个筛选项——该行会自动将新标签滚动进入可视区域。

边缘情况才是这个组件的价值所在

任何带 overflow-x: auto 的 flex 行都能滚动。而一个筛选标签栏的价值恰恰体现在这些细节上:

状态始终掌握在你手中

本库负责滚动,不负责选中状态。标签是你自己的按钮——多选切换用 aria-pressed,单选用普通 state——菜单只要求每一个标签携带一个 itemId。这意味着标签状态可以与你已有的任何东西组合:URL 查询参数、表单库,或服务端驱动的筛选模型。删除一个标签就是 移除一个项目;为其加上退场动画则参见 项目动画示例

移动端:关于页面滚动的一个提醒

在触屏设备上,某些浏览器中在该栏内的横向滑动可能会连带把整个页面也一起拖动。如果你遇到这种情况,阻止页面滚动示例 展示了如何用 touch-action 和 overscroll 约束来锁定这一行为——纯 CSS,不需要任何手势库。

最简模式

标签就是带有 itemId 的切换按钮;通过菜单 API 的 ref 可以将新增的标签滚动进入视图。

ChipBar.tsx
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 注册表条目以受控组件的形式提供——optionsselectedonSelectedChange——以 Tailwind 样式安装进你的 components/ui/

shadcn
npx shadcn@latest add https://react-horizontal-scrolling-menu.dev/r/chip-bar.json

相关示例

全部 21 个示例