About
The smallest switch with the most feel: billing period, chart range, view mode. The thumb is itself glass, so it refracts the pill below it.
Use it when
- Two to five mutually exclusive options.
- Billing toggles, chart ranges, view switches.
Skip it when
- Navigation between pages: use links.
- More than five options.
Install
- 1Add the VibeBox registry to
components.json:{ "registries": { "@vibebox": "https://motion.vibeboxph.com/r/{name}.json" } } - 2Add the component with the shadcn CLI:
npx shadcn@latest add @vibebox/glass-tabs
AI prompt
Everything an AI coding tool needs to rebuild this component the way it looks here: colours, fonts, the art it draws, props and rules.
Read the prompt
Build the VibeBox Motion component "Glass Tabs" (Liquid glass) in a Next.js App Router project with Tailwind CSS v4.
What it is
- A segmented control on glass with a thumb that slides with a small liquid overshoot.
- A tab list on a glass pill. The selected thumb is a brighter second pane that slides by transform with a gentle overshoot; arrow keys, Home and End move the selection.
Install
- `npx shadcn@latest add @vibebox/glass-tabs`
- Registry: add `"@vibebox": "https://motion.vibeboxph.com/r/{name}.json"` to components.json, then run the command. The source lands in `components/vibebox/glass-tabs` and is yours to edit.
Visual spec
- Colours (the VibeBox spectrum, the same in light and dark):
- --spectrum-1: #00c5fc (cyan)
- --spectrum-2: #1d6ef0 (blue)
- --spectrum-3: #5225ed (indigo)
- --spectrum-4: #e434dc (magenta)
- --spectrum-5: #fd3aa9 (pink)
- --spectrum-6: #f98472 (coral)
- --spectrum-7: #fcbe72 (amber)
- Ink surface (dark): background #05070c, card #0a0d15, text #f5f6fa, muted text #a3abbd, border rgba(255,255,255,0.08).
- Paper surface (light): background #f3f0ea, card #fbfaf7, text #0c0e14, muted text #4a4f5c, border rgba(12,14,20,0.12).
- Fonts:
- Display and body: Figtree (400, 500, 600, 700), falling back to ui-sans-serif, system-ui.
- Emphasis: Fraunces italic, for one or two words of a heading.
- Labels and code: JetBrains Mono (400, 500), uppercase labels with 0.14em tracking.
- Shape: radius 8 to 12px on cards and panels; pills may be fully round. Hairline borders, soft glow instead of heavy shadow.
- Motion: animate transform and opacity only; fast ease-out (cubic-bezier(0.16, 1, 0.3, 1)), 140ms for micro-interactions, 320 to 520ms for reveals.
Art and media
- Built from HTML, CSS and inline SVG only; no image files or video.
Props
- `items`: `{ id: string; label: string }[]`. The tabs.
- `value`: `string`. The selected id when controlled.
- `defaultValue`: `string`. The first selection when uncontrolled.
- `onChange`: `(id: string) => void`. Called with the new id.
- `label`: `string` (default "Options"). Accessible name for the tab list.
- `tone`: `"auto" | "dark" | "light"` (default "auto"). Tints the glass and sets the text colour for the page behind it.
- `className`: `string`. Merged with cn(); sets the width and margins.
Behaviour and access
- Reduced motion: The pointer sheen and sliding are off; state changes are instant.
- Keyboard: Arrow keys, Home and End move the selection; one tab stop (roving tabindex).
- Screen readers: role=tablist with role=tab buttons and aria-selected.
- Touch: Targets are 36 px tall; tap selects.
- Budget: about 3 KB of JavaScript, WebGL none, GPU low. It pauses while off screen.
Theming
The surface reads `--g-tint`, `--g-edge`, `--g-ink`, `--g-blur` and `--g-radius` from its root; the demo backdrop uses `--spectrum-*`.
Notes
Add the VibeBox Motion Glass Tabs with `npx shadcn@latest add @vibebox/glass-tabs`. Use it for a two to five option switch such as billing period or chart range: pass `items`, control it with `value` and `onChange`, and give the list a `label`. Keep the roving tabindex, the arrow-key handling and the transform-only thumb.
Rules
- Keep the licence header on every installed file.
- Original code only: do not copy components from other libraries, and do not add GSAP.
- Use tokens for colour, type and radius; no hard-coded brand values.
- Server Components by default; `"use client"` only for the island.Code
2 filescomponents/vibebox/glass-tabs/glass-tabs.client.tsx
/**
* VibeBox Motion · Glass Tabs
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
"use client";
import { useId, useRef, useState, type KeyboardEvent } from "react";
import { cn } from "@/lib/utils";
import { Glass } from "@/lib/vibebox/glass";
export type GlassTab = { id: string; label: string };
export type GlassTabsProps = {
/** The tabs, left to right. */
items?: GlassTab[];
/** The selected id, when you control it. */
value?: string;
/** The first selection when uncontrolled. */
defaultValue?: string;
onChange?: (id: string) => void;
/** An accessible name for the tab list. */
label?: string;
tone?: "auto" | "dark" | "light";
className?: string;
};
const DEFAULT_ITEMS: GlassTab[] = [
{ id: "day", label: "Day" },
{ id: "week", label: "Week" },
{ id: "month", label: "Month" },
{ id: "year", label: "Year" },
];
/**
* A segmented control on glass. The selected thumb is a second, brighter glass pane that slides
* with a small overshoot, by transform only; arrow keys, Home and End move the selection.
*/
export function GlassTabs({ items = DEFAULT_ITEMS, value, defaultValue, onChange, label = "Options", tone, className }: GlassTabsProps) {
const [own, setOwn] = useState(defaultValue ?? items[0]?.id);
const current = value ?? own;
const index = Math.max(0, items.findIndex((item) => item.id === current));
const base = useId();
const buttons = useRef<(HTMLButtonElement | null)[]>([]);
const select = (next: number) => {
const target = items[(next + items.length) % items.length];
if (!target) return;
setOwn(target.id);
onChange?.(target.id);
buttons.current[(next + items.length) % items.length]?.focus();
};
const onKey = (event: KeyboardEvent) => {
if (event.key === "ArrowRight" || event.key === "ArrowDown") select(index + 1);
else if (event.key === "ArrowLeft" || event.key === "ArrowUp") select(index - 1);
else if (event.key === "Home") select(0);
else if (event.key === "End") select(items.length - 1);
else return;
event.preventDefault();
};
return (
<Glass data-vb="glass-tabs" tone={tone} radius={999} still className={cn("inline-block p-1", className)}>
<div role="tablist" aria-label={label} onKeyDown={onKey} className="relative grid" style={{ gridTemplateColumns: `repeat(${items.length}, minmax(0, 1fr))` }}>
<span
aria-hidden="true"
className="pointer-events-none absolute inset-y-0 left-0 rounded-full border border-white/40 bg-gradient-to-b from-white/45 to-white/15 shadow-[inset_0_1px_0_rgb(255_255_255/0.7),0_6px_16px_-6px_rgb(0_0_0/0.45)] transition-transform duration-500 ease-[cubic-bezier(0.34,1.45,0.5,1)] motion-reduce:transition-none"
style={{ width: `${100 / items.length}%`, transform: `translateX(${index * 100}%)` }}
/>
{items.map((item, position) => (
<button
key={item.id}
ref={(node) => {
buttons.current[position] = node;
}}
id={`${base}-${item.id}`}
role="tab"
type="button"
aria-selected={position === index}
tabIndex={position === index ? 0 : -1}
onClick={() => select(position)}
className={cn(
"relative z-10 h-9 rounded-full px-4 text-[0.88rem] font-medium transition-opacity focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
position === index ? "opacity-100" : "opacity-65 hover:opacity-90",
)}
>
{item.label}
</button>
))}
</div>
</Glass>
);
}components/vibebox/glass-tabs/index.tsx
/**
* VibeBox Motion · Glass Tabs
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
export * from "./glass-tabs.client";Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | { id: string; label: string }[] | The tabs. | |
| value | string | The selected id when controlled. | |
| defaultValue | string | The first selection when uncontrolled. | |
| onChange | (id: string) => void | Called with the new id. | |
| label | string | "Options" | Accessible name for the tab list. |
| tone | "auto" | "dark" | "light" | "auto" | Tints the glass and sets the text colour for the page behind it. |
| className | string | — | Merged with cn(); sets the width and margins. |
Accessibility and motion
- Keyboard
- Arrow keys, Home and End move the selection; one tab stop (roving tabindex).
- Screen readers
- role=tablist with role=tab buttons and aria-selected.
- Touch
- Targets are 36 px tall; tap selects.
- Reduced motion
- The pointer sheen and sliding are off; state changes are instant.
Related

Glass Controls
FreeA control-centre panel: glass toggle tiles and sliders that are real range inputs.
Liquid glass · CSS

Glass Dialog
FreeA confirmation dialog on glass over a dimmed scrim, with focus kept inside and Escape to cancel.
Liquid glass · CSS

Glass Dock
FreeA floating dock of glass icons that swell toward the pointer, with labels and badges.
Liquid glass · CSS