---
title: "React scrollable tabs — no Material UI required"
description: "Scrollable tabs in React with native scrolling: the active tab centers itself, arrows appear only when needed, free-form tab content. Live demo and source."
canonical: "https://react-horizontal-scrolling-menu.dev/scrollable-tabs"
image: "https://react-horizontal-scrolling-menu.dev/og.png"
---

# React scrollable tabs that scroll like the browser

A tab strip stops fitting the moment your product grows past six tabs. The fix is not a smaller font — it is a strip that scrolls: overflow rides the browser, clicking a tab centers it, and arrows show up only when there is somewhere to go.

Overview

Getting started

Visibility

Arrows

Drag to scroll

RTL

apiRef

Helpers

TypeScript

Testing

Changelog

FAQ

Click a tab near the edge — it scrolls itself to the center.

[Edit this example live in Storybook](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-centeronclick--center-on-click)

## The one behavior that matters: center on select

A scrollable tab strip lives or dies on what happens when you click a tab at the edge: it should glide to the middle, revealing its neighbors on both sides. That is one call here — `scrollToItem(el, 'smooth', 'center')` — wired in the [center-on-click example](https://react-horizontal-scrolling-menu.dev/examples/center-on-click.md). Restoring the active tab on mount is the same call with `'auto'`, shown in [save & restore position](https://react-horizontal-scrolling-menu.dev/examples/save-restore-position.md).

The arrows come from the same visibility data: `useLeftArrowVisible` is false only while the first tab is off screen, so the left arrow renders exactly when it is useful. No measurement code, no resize observers of your own.

## If you are outgrowing MUI scrollable tabs

Material UI's `variant="scrollable"` tabs are the right answer inside Material's design system — until your "tabs" stop being tabs. MUI welds the strip to Tabs semantics: a `value`/`onChange` pair, tab panels, and scroll buttons that MUI hides on mobile by default. The moment your row holds chips, cards, avatars or mixed content, or needs drag-to-scroll, or needs to know which items are visible, you are fighting the component rather than using it.

This library is the layer below that: a scrolling row with visibility tracking, no opinion about what a "tab" is. Your tab is any component with an `itemId` — style it with Tailwind, MUI's own `styled`, or plain CSS. Selection state stays yours, exactly like the demo above keeps it in one `useState`.

## Accessibility is mostly free — mind the two gaps

Because the strip is a native scroll container, keyboard focus, screen-reader reading order and RTL come from the platform — moving focus through tabs scrolls them into view with zero code, and [RTL](https://react-horizontal-scrolling-menu.dev/examples/rtl.md) needs no configuration. Two things stay on you, same as any tab UI: pick your ARIA pattern (`role="tablist"` if real panels switch, `aria-current` if the "tabs" are navigation), and keep the [drag-to-scroll](https://react-horizontal-scrolling-menu.dev/examples/mouse-drag.md) recipe's click suppression so a drag release never activates a tab.

## The pattern, minimal

Tabs are plain buttons with an `itemId`; selecting one centers it. This is the whole idea — the demo above adds styling and drag.

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

## Or install it as a shadcn component

The [scroll-tabs](https://react-horizontal-scrolling-menu.dev/r/scroll-tabs.json) registry item ships this pattern data-driven — pass `tabs`, `value`, `onValueChange` — as an editable component in your `components/ui/`:

shadcn

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

## Related examples

-   [Center the clicked itemscrollToItem with inline: center — the scrollable-tabs pattern.](https://react-horizontal-scrolling-menu.dev/examples/center-on-click.md)
-   [Save and restore scroll positionKeep the scroll offset across unmounts and page reloads.](https://react-horizontal-scrolling-menu.dev/examples/save-restore-position.md)
-   [Drag to scroll with the mouseMouse drag that still lets item clicks work.](https://react-horizontal-scrolling-menu.dev/examples/mouse-drag.md)

[All 21 examples](https://react-horizontal-scrolling-menu.dev/examples.md)

---

More examples: <https://react-horizontal-scrolling-menu.dev/examples.md>
Library summary for LLMs: <https://react-horizontal-scrolling-menu.dev/llms.txt>
