Skip to content

FreeIn reviewv1.0.0

Glass Dock

A floating dock of glass icons that swell toward the pointer, with labels and badges.
npx shadcn@latest add @vibebox/glass-dock

Controls

Tone
52
1.55

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

  1. 1Add the VibeBox registry to components.json:
    {
      "registries": {
        "@vibebox": "https://motion.vibeboxph.com/r/{name}.json"
      }
    }
  2. 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 files
components/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

PropTypeDefaultDescription
items{ id: string; label: string; glyph: GlassGlyph; badge?: number }[]The destinations, left to right.
activestringThe open item's id; uncontrolled when left out.
onSelect(id: string) => voidCalled with the chosen id.
sizenumber52Resting icon size in pixels.
magnifynumber1.55How 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.
classNamestring—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.
  • A glass pill header with a wordmark, four links and a white call to action

    A floating pill header on glass with a highlight that glides between links.

    Liquid glass · CSS

  • A glass control centre with four toggle tiles and two sliders

    A control-centre panel: glass toggle tiles and sliders that are real range inputs.

    Liquid glass · CSS

  • A glass confirmation dialog with two buttons over a dimmed colourful page

    A confirmation dialog on glass over a dimmed scrim, with focus kept inside and Escape to cancel.

    Liquid glass · CSS

↑ ↓ to move↵ to openesc to close