Skip to content
WANCRAFT

D.137Navigation & Menus

Scrollable Tabs

A tab strip for many tabs: edge fades and scroll buttons, a sliding indicator, wheel-to-horizontal scrolling and closable tabs.

NewReactMotionTailwindadvanced

Overview

Scrollable Tabs handles the tab strip that has outgrown its container. The strip scrolls natively with touch and trackpads, turns vertical mouse-wheel movement into horizontal scrolling while it still has room to move, and fades out at whichever edge can scroll, with small buttons that page left and right. The selected tab is always brought into view by setting the strip's scrollLeft (never scrollIntoView, so the page stays put), and the indicator slides between tabs as a shared-layout spring that stays correct while the strip is scrolled. It follows the WAI-ARIA tabs pattern with roving tabindex, arrow keys, Home and End, automatic or manual activation and linked tab panels; tabs can be closable, with a close glyph, middle-click and the Delete key, after which a neighbour is selected and focused. It works controlled or uncontrolled.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/scrollable-tabs.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/scrollable-tabs.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/scrollable-tabs.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { ScrollableTabs, type ScrollableTabItem } from "@/components/ui/scrollable-tabs";

export function ScrollableTabsExample() {
  const items: ScrollableTabItem[] = [
    "Overview", "Activity", "Deployments", "Analytics", "Logs", "Storage",
    "Domains", "Integrations", "Members", "Billing", "Security", "Settings",
  ].map((name) => ({ value: name.toLowerCase(), label: name, content: <p>{name} panel</p> }));

  return (
    <div className="w-full max-w-xl">
      <ScrollableTabs items={items} label="Project sections" />
    </div>
  );
}

03Props

PropTypeDefaultDescription
itemsScrollableTabItem[]—Required. { value, label, textLabel?, icon?, content?, closable?, disabled? }. Panels render when any item has content.
value / defaultValuestring—Selected value (controlled) or the initial value (uncontrolled; defaults to the first enabled tab).
onValueChange(value: string) => void—Called when the selection changes.
closablebooleanfalseClose glyph on every tab, middle-click and Delete / Backspace close. Per-item closable overrides it.
onClose(value: string) => void—Remove the item here. A closed selected tab hands selection (and focus) to its neighbour first.
variant"underline" | "pill""underline"Underline bar, or a raised pill on a tray.
activation"automatic" | "manual""automatic"Select on arrow-key focus, or wait for Enter / Space.
showScrollButtonsbooleantrueEdge buttons that page the strip while it overflows.
fadeEdgesbooleantrueMask the edges that can scroll.
wheelScrollbooleantrueVertical wheel scrolls the strip horizontally until it reaches an end.
spring{ stiffness; damping; mass }{ 420, 36, 0.8 }Spring for the sliding indicator.
accentstring"#FF7448"Indicator and focus ring colour.
labelstring"Tabs"Accessible name of the tablist.
...propsHTMLAttributes<HTMLDivElement>—Forwarded to the root element.

04Features

  • 01Native horizontal scrolling (touch drag, trackpad) with a hidden scrollbar
  • 02Edge fades and page buttons that appear only on edges that can scroll
  • 03Vertical wheel scrolls the strip sideways, then hands back to the page at either end
  • 04Selected and focused tabs are kept in view by setting scrollLeft, never scrollIntoView
  • 05Shared-layout spring indicator that stays aligned while the strip scrolls (layoutScroll)
  • 06Closable tabs: close glyph, middle-click and Delete, with neighbour selection and focus
motion · springmotion · layoutinput · clickinput · keyboardinput · scrollinput · drag

05Accessibility

  • WAI-ARIA tabs: role="tablist" / "tab" / "tabpanel" with aria-selected, aria-controls and aria-labelledby.
  • Roving tabindex; ArrowLeft / ArrowRight (wrapping), Home and End move between enabled tabs; manual mode selects with Enter or Space.
  • Closable tabs are described as "Press Delete to close this tab"; closing is announced in a polite live region and focus moves to the neighbouring tab.
  • Scroll buttons are labelled but kept out of the tab order, since the arrow keys already scroll the strip; the close glyph is aria-hidden.
  • prefers-reduced-motion makes the indicator jump, scrolling instant and panels appear without a fade.

06Limitations

  • Left-to-right layouts only: edge detection and wheel mapping assume scrollLeft grows to the right.
  • Closing is controlled: the component reports onClose and the parent removes the item.