About
Navigation as a physical object: a dock that leans toward the pointer. Use it for app-like sites, portfolios and dashboards where a few destinations deserve a handsome home.
Use it when
- Five to eight primary destinations.
- App-style portfolios and launchers.
Skip it when
- Long menus: use a header or a command menu.
- As the only navigation on a content site.
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-dock
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 Dock" (Liquid glass) in a Next.js App Router project with Tailwind CSS v4.
What it is
- A floating dock of glass icons that swell toward the pointer, with labels and badges.
- A dock on liquid glass whose icons swell on a bell curve toward the pointer (transform only), with hover and focus labels, a badge and an open-app dot. Touch and reduced motion keep it flat.
Install
- `npx shadcn@latest add @vibebox/glass-dock`
- Registry: add `"@vibebox": "https://motion.vibeboxph.com/r/{name}.json"` to components.json, then run the command. The source lands in `components/vibebox/glass-dock` 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; glyph: GlassGlyph; badge?: number }[]`. The destinations, left to right.
- `active`: `string`. The open item's id; uncontrolled when left out.
- `onSelect`: `(id: string) => void`. Called with the chosen id.
- `size`: `number` (default 52). Resting icon size in pixels.
- `magnify`: `number` (default 1.55). How far the icon under the pointer grows; 1 keeps them flat.
- `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: Each icon is a native button with a visible focus ring and its label shown on focus.
- Screen readers: A nav landmark named Dock; icons have accessible names, the open one has aria-current.
- Touch: No magnification on touch; targets are the full icon size.
- 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 Dock with `npx shadcn@latest add @vibebox/glass-dock`. Pass `items` with an id, label and one of the built-in glyph names for each destination, mark the open one with `active`, and place it fixed at the bottom centre of the page. Keep the bell-curve magnify (transform only), the labels on focus, and the flat mode for touch and reduced motion.
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-dock/glass-dock.client.tsx
/**
* VibeBox Motion · Glass Dock
* 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 { useRef, useState, type PointerEvent } from "react";
import { cn } from "@/lib/utils";
import { Glass, GlassGlyphIcon, type GlassGlyph } from "@/lib/vibebox/glass";
export type GlassDockItem = { id: string; label: string; glyph: GlassGlyph; badge?: number };
export type GlassDockProps = {
/** The apps or destinations, left to right. */
items?: GlassDockItem[];
/** The id of the open one, marked with a dot. Uncontrolled when left out. */
active?: string;
/** Called with an item's id when it is chosen. */
onSelect?: (id: string) => void;
/** Resting size of an icon in pixels. */
size?: number;
/** How far the icon under the pointer grows (1 keeps them flat). */
magnify?: number;
tone?: "auto" | "dark" | "light";
className?: string;
};
const DEFAULT_ITEMS: GlassDockItem[] = [
{ id: "home", label: "Home", glyph: "home" },
{ id: "search", label: "Search", glyph: "search" },
{ id: "mail", label: "Mail", glyph: "mail", badge: 3 },
{ id: "music", label: "Music", glyph: "music" },
{ id: "camera", label: "Camera", glyph: "camera" },
{ id: "settings", label: "Settings", glyph: "settings" },
];
/**
* A dock of glass icons that swell toward the pointer, like the one on a desktop. The size follows
* a bell curve of the distance, set with transform only; touch and reduced motion keep them flat.
*/
export function GlassDock({ items = DEFAULT_ITEMS, active, onSelect, size = 52, magnify = 1.55, tone, className }: GlassDockProps) {
const [own, setOwn] = useState(items[0]?.id);
const current = active ?? own;
const refs = useRef<(HTMLLIElement | null)[]>([]);
const root = useRef<HTMLElement | null>(null);
const frame = useRef(0);
/*
* Each icon grows on a bell curve of its distance from the pointer. Growing by transform alone
* would let a big icon sit on its neighbours, so every icon is also shifted outward by the growth
* of the ones before it (and the whole row re-centred): the icons stay side by side. Distances use
* the resting centres (offsetLeft), so the curve does not chase its own movement.
*/
const apply = (clientX: number | null) => {
window.cancelAnimationFrame(frame.current);
frame.current = window.requestAnimationFrame(() => {
const nodes = refs.current.filter((node): node is HTMLLIElement => Boolean(node));
const host = root.current;
if (!host) return;
const left = host.getBoundingClientRect().left;
const scales = nodes.map((node) => {
if (clientX === null) return 1;
const centre = node.offsetLeft + node.offsetWidth / 2;
const distance = Math.abs(clientX - left - centre);
return 1 + (magnify - 1) * Math.exp(-((distance / (size * 1.1)) ** 2));
});
const growth = scales.map((value) => (value - 1) * size);
const total = growth.reduce((sum, value) => sum + value, 0);
let before = 0;
nodes.forEach((node, index) => {
const g = growth[index] ?? 0;
node.style.setProperty("--s", (scales[index] ?? 1).toFixed(3));
node.style.setProperty("--x", `${(before + g / 2 - total / 2).toFixed(1)}px`);
before += g;
});
});
};
const onMove = (event: PointerEvent) => {
if (event.pointerType !== "mouse" || magnify <= 1) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
apply(event.clientX);
};
return (
<Glass
as="nav"
data-vb="glass-dock"
aria-label="Dock"
tone={tone}
radius={Math.round(size * 0.42)}
still
rootRef={root}
onPointerMove={onMove}
onPointerLeave={() => apply(null)}
className={cn("inline-flex items-end px-3 py-2", className)}
>
<ul className="flex items-end gap-2">
{items.map((item, index) => (
<li
key={item.id}
ref={(node) => {
refs.current[index] = node;
}}
className="group relative origin-bottom [transform:translate3d(var(--x,0px),0,0)_scale(var(--s,1))] transition-transform duration-150 ease-out motion-reduce:transition-none"
>
<button
type="button"
aria-label={item.label}
aria-current={current === item.id ? "true" : undefined}
onClick={() => {
setOwn(item.id);
onSelect?.(item.id);
}}
style={{ width: size, height: size, borderRadius: Math.round(size * 0.3) }}
className="relative grid place-items-center border border-white/25 bg-gradient-to-b from-white/30 to-white/5 shadow-[inset_0_1px_0_rgb(255_255_255/0.5)] transition-colors hover:from-white/40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
>
<GlassGlyphIcon name={item.glyph} className="size-[46%]" />
{item.badge ? (
<span className="absolute -top-1 -right-1 grid h-4 min-w-4 place-items-center rounded-full bg-[#ff4d6d] px-1 font-mono text-[0.6rem] leading-none font-semibold text-white">{item.badge}</span>
) : null}
</button>
<span aria-hidden="true" className={cn("mx-auto mt-1 block size-1 rounded-full bg-white transition-opacity", current === item.id ? "opacity-90" : "opacity-0")} />
<span
aria-hidden="true"
className="pointer-events-none absolute -top-8 left-1/2 -translate-x-1/2 rounded-md bg-black/60 px-2 py-0.5 text-[0.7rem] whitespace-nowrap text-white opacity-0 backdrop-blur-sm transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
>
{item.label}
</span>
</li>
))}
</ul>
</Glass>
);
}components/vibebox/glass-dock/index.tsx
/**
* VibeBox Motion · Glass Dock
* 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-dock.client";Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | { id: string; label: string; glyph: GlassGlyph; badge?: number }[] | The destinations, left to right. | |
| active | string | The open item's id; uncontrolled when left out. | |
| onSelect | (id: string) => void | Called with the chosen id. | |
| size | number | 52 | Resting icon size in pixels. |
| magnify | number | 1.55 | How far the icon under the pointer grows; 1 keeps them flat. |
| 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
- Each icon is a native button with a visible focus ring and its label shown on focus.
- Screen readers
- A nav landmark named Dock; icons have accessible names, the open one has aria-current.
- Touch
- No magnification on touch; targets are the full icon size.
- Reduced motion
- The pointer sheen and sliding are off; state changes are instant.
Related

Glass Nav
FreeA floating pill header on glass with a highlight that glides between links.
Liquid glass · CSS

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