Skip to content
WANCRAFT

D.14Navigation & Menus

Morphing Tabs

Accessible tabs with a selection indicator that flows between tabs on a spring.

ReactMotionTailwindintermediate

Overview

Morphing Tabs renders a WAI-ARIA tab list whose indicator is a single shared layout element: when the selection changes it stretches and settles into the new tab with a spring instead of jumping. Each instance scopes its layoutId with useId, so any number of tab sets can live on one page. It supports controlled and uncontrolled use, automatic or manual activation, roving tabindex with arrow keys, Home and End, and optional animated panels. Three variants: accent pill, neutral segment and underline.

01Installation

Method 01 · Registry

CLI verified

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

npx shadcn@latest add https://wancraft.my/r/morphing-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/morphing-tabs.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
1 Oct 2026
Updated
—

02Usage

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

example.tsx
import { MorphingTabs, type MorphingTabItem } from "@/components/ui/morphing-tabs";

export function MorphingTabsExample() {
  const tabs: MorphingTabItem[] = [
    { value: "overview", label: "Overview", content: <p>Overview panel</p> },
    { value: "activity", label: "Activity", content: <p>Activity panel</p> },
    { value: "settings", label: "Settings", content: <p>Settings panel</p> },
  ];

  return (
    <MorphingTabs items={tabs} label="Project sections" />
  );
}

03Props

PropTypeDefaultDescription
itemsMorphingTabItem[]—{ value: string; label: ReactNode; icon?: ReactNode; content?: ReactNode; disabled?: boolean }.
valuestring—Selected value (controlled).
defaultValuestring—Initially selected value (uncontrolled). Defaults to the first enabled item.
onValueChange(value: string) => void—Called when the selection changes.
variant"pill" | "segment" | "underline""pill"Visual style of the indicator.
activation"automatic" | "manual""automatic"Automatic selects on arrow-key focus; manual waits for Enter or Space.
spring{ stiffness: number; damping: number; mass: number }{ stiffness: 380, damping: 32, mass: 0.8 }Spring that morphs the indicator.
accentstring"#FF7448"Pill fill, underline and focus ring colour.
fullWidthbooleanfalseStretch tabs to fill the available width.
labelstring—Accessible name of the tab list.
...propsHTMLAttributes<HTMLDivElement>—Native attributes are forwarded to the wrapper.

04Features

  • 01Shared-layout indicator (motion layoutId) scoped per instance with useId
  • 02Controlled (value + onValueChange) and uncontrolled (defaultValue) APIs
  • 03Automatic or manual activation
  • 04Optional tab panels with a short enter transition
  • 05Pill, segment and underline variants; disabled tabs are skipped
motion · layoutmotion · springinput · clickinput · keyboardinput · focus

05Accessibility

  • Implements the WAI-ARIA tabs pattern: tablist, tab and tabpanel roles with aria-selected, aria-controls and aria-labelledby.
  • Roving tabindex: Tab enters the selected tab; Left / Right wrap, Home / End jump.
  • Panels are focusable (tabIndex 0) and hidden with the hidden attribute when inactive.
  • With prefers-reduced-motion the indicator moves instantly.