Skip to content
WANCRAFT

D.166Navigation & Menus

Swipe Tabs

Mobile tabs with side-by-side panels on native scroll-snap and an indicator that tracks the swipe continuously.

NewReactCSSTailwindintermediate

Overview

Swipe Tabs puts every panel side by side in a native scroll-snap track, so swiping between them is the browser's own momentum scrolling rather than a JavaScript gesture. While you swipe, the indicator under the tab bar follows your thumb continuously: the fractional scroll position is written to CSS custom properties each frame, with no React renders, and the tab labels cross-fade towards the panel you are heading for. The selection only changes once scrolling settles; tapping a tab scrolls to its panel. Each panel scrolls vertically on its own, and once settled the inactive panels are inert so focus and assistive technology stay in the visible one. The tab bar follows the WAI-ARIA tabs pattern with arrow-key navigation.

01Installation

Method 01 · Registry

CLI verified

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

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

Method 02 · Copy and paste

  1. 1. No extra dependencies — React and Tailwind CSS only.
  2. 2. Copy the source from the Code tab into components/ui/swipe-tabs.tsx.
  3. 3. Import it and adjust props.
Dependencies
None
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 { SwipeTabs, type SwipeTab } from "@/components/ui/swipe-tabs";

export function SwipeTabsExample() {
  const tabs: SwipeTab[] = [
    { id: "for-you", label: "For you", content: <div className="p-4">Stories picked for you</div> },
    { id: "following", label: "Following", content: <div className="p-4">People you follow</div> },
    { id: "saved", label: "Saved", content: <div className="p-4">Saved for later</div> },
  ];

  return (
    <div className="h-[520px] w-full max-w-sm overflow-hidden rounded-3xl border">
      <SwipeTabs tabs={tabs} label="Journal sections" className="h-full" />
    </div>
  );
}

03Props

PropTypeDefaultDescription
tabsSwipeTab[]—Required. { id, label, icon?, content }. Each panel scrolls vertically on its own.
valuestring—Selected tab id (controlled).
defaultValuestring—Initially selected tab id (uncontrolled). Defaults to the first tab.
onValueChange(id: string) => void—Called when a tab is chosen or a swipe settles on a new panel.
labelstring"Sections"Accessible name of the tab list.
indicator"line" | "pill""line"Underline or segmented pill indicator.
stretchbooleantrueThe indicator stretches mid-swipe and contracts as it lands.
swipeablebooleantrueAllow swiping between panels; when false only the tabs switch.
accentstring"#FF7448"Indicator and focus ring colour.
...propsHTMLAttributes<HTMLDivElement>—Forwarded to the root. Give it (or its parent) a height: panels fill the remaining space.

04Features

  • 01Panels in a native scroll-snap track: platform momentum, no gesture library
  • 02Scroll-linked indicator and label cross-fade written to CSS variables every frame, without React renders
  • 03Selection changes only when the scroll settles (scrollend with a timeout fallback)
  • 04Tapping a tab scrolls to its panel; controlled value changes do too
  • 05Inactive panels are inert after settling; each panel scrolls vertically on its own
  • 06Line or pill indicator with an optional mid-swipe stretch
motion · scroll-linkedinput · scrollinput · draginput · clickinput · keyboard

05Accessibility

  • WAI-ARIA tabs: a labelled role="tablist" of role="tab" buttons with aria-selected and aria-controls, and role="tabpanel" panels labelled by their tab.
  • Roving tab stop on the selected tab; Left / Right wrap between tabs, Home / End jump, and each move selects and scrolls to the panel.
  • Panels other than the selected one are inert, so Tab, find-in-page and screen readers stay in the visible panel.
  • Panels are focusable (tabindex 0) with a visible focus ring, so keyboard users can scroll long content.
  • With prefers-reduced-motion, tab changes jump instantly instead of smooth-scrolling; the indicator only moves with your own swipe.

06Limitations

  • Tabs share the width equally, so this suits 2–5 short labels; use Scrollable Tabs for many tabs.
  • Left-to-right layouts only.