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.
Edite este exemplo ao vivo no Storybook
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.
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/:
npx shadcn@latest add https://react-horizontal-scrolling-menu.dev/r/scroll-tabs.json