A filter-chip bar that scrolls, in React

The chip row under every search bar — YouTube topics, store filters, tag pickers — is a single-line scroll container full of toggle buttons. The hard 10% is what happens at the edges: new chips appearing off-screen, drags that must not toggle anything, and arrows that know when they are pointless.

Add a filter — the row scrolls the new chip into view by itself.

The edge cases are the feature

Any flex row with overflow-x: auto scrolls. A chip bar earns its keep on the details:

State stays in your hands

The library scrolls; it does not own selection. Chips are your buttons — aria-pressed for multi-select toggles, plain state for single-select — and the menu only needs each one to carry an itemId. That means chip state composes with whatever you already have: URL search params, a form library, a server-driven filter model. Deleting a chip is removing an item; animating it out is the items-animation example.

Mobile: one warning about body scroll

On touch screens a horizontal swipe inside the bar can drag the page sideways with it on some browsers. If you see that, the prevent-body-scroll example shows the touch-action and overscroll containment to lock it down — CSS only, no gesture library.

The pattern, minimal

Chips are toggle buttons with an itemId; a ref to the menu API scrolls a newly added chip into view.

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>
  );
}

Or install it as a shadcn component

The chip-bar registry item ships this as a controlled component — options, selected, onSelectedChange — Tailwind-styled in your components/ui/:

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

Related examples

All 21 examples