Abas roláveis em React que rolam como o navegador

Uma barra de abas para de caber assim que seu produto passa de seis abas. A solução não é uma fonte menor — é uma barra que rola: o overflow fica por conta do navegador, clicar em uma aba a centraliza, e as setas aparecem só quando há para onde ir.

Clique em uma aba perto da borda — ela rola sozinha até o centro.

O único comportamento que importa: centralizar ao selecionar

Uma barra de abas rolável vive ou morre pelo que acontece quando você clica em uma aba na borda: ela deve deslizar até o meio, revelando as vizinhas dos dois lados. Aqui isso é uma única chamada — scrollToItem(el, 'smooth', 'center') — ligada no exemplo center-on-click. Restaurar a aba ativa ao montar o componente é a mesma chamada com 'auto', mostrada em salvar e restaurar posição.

As setas vêm dos mesmos dados de visibilidade: useLeftArrowVisible só é falso enquanto a primeira aba está fora da tela, então a seta esquerda é renderizada exatamente quando é útil. Sem código de medição, sem resize observers próprios.

Se você está deixando as abas roláveis do MUI para trás

As abas variant="scrollable" do Material UI são a resposta certa dentro do design system do Material — até suas "abas" pararem de ser abas. O MUI solda a barra à semântica de Tabs: um par value/onChange, painéis de aba e botões de rolagem que o MUI esconde no mobile por padrão. No momento em que sua fileira passa a ter chips, cards, avatares ou conteúdo misto, ou precisa de arraste para rolar, ou precisa saber quais itens estão visíveis, você está lutando contra o componente em vez de usá-lo.

Esta biblioteca é a camada abaixo disso: uma fileira rolável com rastreamento de visibilidade, sem opinião sobre o que é uma "aba". Sua aba é qualquer componente com um itemId — estilize com Tailwind, o styled do próprio MUI, ou CSS puro. O estado de seleção continua seu, exatamente como a demo acima o mantém em um único useState.

Acessibilidade é quase de graça — atenção às duas lacunas

Como a barra é um contêiner de rolagem nativa, o foco de teclado, a ordem de leitura do leitor de tela e o RTL vêm da plataforma — mover o foco pelas abas as rola para a área visível sem nenhum código, e o RTL não precisa de configuração. Duas coisas continuam por sua conta, como em qualquer UI de abas: escolher seu padrão ARIA (role="tablist" se painéis reais forem trocados, aria-current se as "abas" forem navegação) e manter a supressão de clique da receita de arraste para rolar para que soltar um arraste nunca ative uma aba.

O padrão, no mínimo

Abas são botões comuns com um itemId; selecionar uma a centraliza. Essa é a ideia toda — a demo acima adiciona estilização e arraste.

ScrollTabs.tsx
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>
  );
}

Ou instale como um componente shadcn

O item de registro scroll-tabs entrega esse padrão orientado a dados — passe tabs, value, onValueChange — como um componente editável no seu components/ui/:

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

Exemplos relacionados

Todos os exemplos (21)