Glide tabs

Tabs with a pill that has mass. The edge moving toward the new tab leads on a spring and the far edge trails it, so the pill stretches across the gap and snaps back. A second set of labels rides on the pill and is clipped to it, so each label changes colour exactly where the pill's edge crosses it. Panels slide the way the pill went, and the height eases between them.

Overview

Three deploys today. The last one went out at 14:02 and is serving all traffic.

Installation

Terminalbash
npx @neelshha/ui@latest add glide-tabs

Usage

page.tsxtsx
import { GlideTabs } from "@/components/ui/glide-tabs";

<GlideTabs
  aria-label="Project"
  tabs={[
    { value: "overview", label: "Overview", content: <Overview /> },
    { value: "activity", label: "Activity", content: <Activity /> },
    { value: "settings", label: "Settings", content: <Settings /> },
  ]}
/>

Tones

Leave out content for a segmented control. The ink pill makes the colour change at its edge easiest to see.

page.tsxtsx
<GlideTabs aria-label="Range" tabs={range} defaultValue="week" />
<GlideTabs
  aria-label="Range"
  tabs={range}
  defaultValue="week"
  tone="ink"
/>
<GlideTabs
  aria-label="Range"
  tabs={range}
  defaultValue="week"
  tone="key"
/>

With icons

Labels are any content. An icon goes on both copies of the label, so it changes colour with the text.

page.tsxtsx
<GlideTabs
  aria-label="View"
  tone="ink"
  tabs={[
    { value: "list", label: <><List />List</> },
    { value: "grid", label: <><LayoutGrid />Grid</> },
    { value: "board", label: <><Columns3 />Board</> },
  ]}
/>

Keyboard

The list follows the WAI-ARIA tabs pattern. Tab moves into the list and on to the panel. The arrow keys move and select, and wrap at the ends. Home and End jump to the first and last tab. Disabled tabs are skipped.

API

Other props go to the outer div. With reduced motion, the pill and the panels change at once.

PropTypeDefaultDescription
tabsGlideTab[]Each tab has a value and a label, and can have content and disabled. With no content on any tab, it is a segmented control.
defaultValue / valuestringUncontrolled default, or controlled value. Defaults to the first enabled tab.
onValueChange(value: string) => voidFires with the next value on click and on the arrow keys.
tone"chrome" | "ink" | "key""chrome"The pill's material: silver chrome, solid ink, or the blue key.
aria-labelstringNames the tab list. Use aria-labelledby to point at a visible heading instead.