Navigation
Dock
A macOS-style dock — icon tiles magnify as the cursor approaches, with spring physics and pure-CSS label bubbles.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/dock.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "Dock" component using the motion
package (import from "motion/react").
Contract
- export function Dock({ items, magnification = 64, baseSize = 40, className,
...rest }) — rest props spread onto the root div, className merged via cn().
- items: { id: string; label: string; icon: React.ReactNode;
onClick?: () => void; href?: string }[]. An item with href renders a real
<a>; otherwise a <button type="button">. Both call onClick.
- magnification = peak tile size (px) directly under the cursor;
baseSize = resting tile size (px).
Behavior
- The container owns one useMotionValue(Infinity) mouseX: mousemove writes
e.clientX, mouseleave resets to Infinity. MotionValues bypass React state,
so tracking never re-renders the tree.
- Each item is its own subcomponent (hooks cannot live in a map callback).
It measures its center via ref + getBoundingClientRect inside a
useTransform, derives distance = mouseX - centerX, then maps
[-150, 0, 150] -> [baseSize, magnification, baseSize] (useTransform clamps
outside the range, so Infinity collapses every tile to baseSize).
- A useSpring (mass 0.1, stiffness 170, damping 14) smooths the size; tile
width and height both bind to it, and an inner icon layer binds to size/2.
- Keyboard parity: on focus the tile writes its own center into the shared
mouseX so the focused tile (and neighbors) magnify exactly like hover;
blur resets to Infinity. No separate focus animation code path.
- Label bubble floats above the tile on hover and :focus-visible with pure
CSS (group-hover / group-focus-visible opacity + translate) — no portal,
no radix. The tile is position:relative, so no positioning math is needed.
- prefers-reduced-motion (motion's useReducedMotion): bypass the transform
pipeline entirely — fixed baseSize tiles, mouse tracking not attached;
clicks, focus rings and label bubbles keep working.
Rendering & styling
- Bar: flex items-end gap-2 rounded-2xl border bg-background/60 px-2 pb-2
pt-1 backdrop-blur, role="toolbar" with an aria-label.
- Tile: rounded-xl bg-muted hover:bg-accent transition-colors, icon centered;
focus-visible:ring-2 ring-ring. Icon and bubble layers are aria-hidden;
the accessible name is aria-label={label} on the button/anchor itself.
- Bubble: bg-popover text-popover-foreground border shadow-sm text-xs,
pointer-events-none, whitespace-nowrap.
- Semantic tokens only — no hex/rgb/oklch; merge consumer className via cn().
- The bar grows upward while magnifying; in layouts where reflow matters,
place it in a fixed-height items-end wrapper sized to magnification.
Customization levers
- Falloff curve: the [-150, 0, 150] input range is the feel knob — widen to
~200 for a softer, wavier ripple across neighbors, narrow to ~100 so only
the hovered tile reacts. Add intermediate stops (e.g. [-150, -75, 0, ...])
for a non-linear decay.
- Spring feel: stiffness 170 / damping 14 is snappy; lower stiffness (~120)
reads lazier, higher damping kills overshoot.
- Size scale: baseSize/magnification are plain px props — 48/80 for a
hero-sized dock, 32/48 for a compact toolbar; icon layer stays at size/2.
- Vertical dock: swap the axis — track clientY, measure bounds.y + height/2,
container flex-col items-end on a screen edge, bubble to the tile's side.
- Separators and running indicators: insert a self-stretching 1px border-l
divider between item groups, and an absolutely-positioned size-1 rounded
dot (bg-foreground/60) under active items driven by an activeIds prop.
- Surface: swap bg-background/60 + backdrop-blur for a solid bg-card if the
dock never overlays content; tiles can go bg-transparent hover:bg-muted
for a flatter look.Concepts
- Proximity magnification — every tile derives its size from its horizontal distance to one shared cursor value, so neighbors swell in a smooth falloff instead of a single tile popping on hover.
- MotionValue pipeline — mouseX → useTransform → useSpring writes styles outside the React render cycle; a 60fps mousemove stream causes zero re-renders of the item tree.
- Focus parity — keyboard focus feeds the focused tile's own center into the same mouseX value, so focus magnifies through the identical spring path as hover: one code path, no special-cased animation.
- Infinity as "no cursor" — resetting mouseX to Infinity makes every distance clamp to base size, so mouse-leave settle is the same spring relaxing, not a separate exit animation.
- Pure-CSS label bubble — labels reveal on group-hover / group-focus-visible with an opacity + translate transition; the tile itself is the positioning context, so no portal or floating-ui is needed.
- Reduced-motion fallback — under prefers-reduced-motion the transform pipeline is bypassed rather than slowed: tiles hold base size while clicks, focus rings and labels keep full function.
Partial Failure Summary
The receipt for one bulk action that half worked — counts as a sentence, failures grouped by cause, and a retry scoped by whether the successes were committed or rolled back.
Animated Tabs
A controlled tab strip whose active indicator glides between tabs — underline and pill variants via motion layout animation.