Navigation
Dock
A macOS-style dock whose icons magnify as the cursor sweeps past them.
import { Dock, DockItem } from "@/components/ui/dock";
import { Compass, Folder, Mail, Music, Settings, Terminal } from "lucide-react";
const apps = [
{ label: "Finder", Icon: Folder },
{ label: "Terminal", Icon: Terminal },
{ label: "Mail", Icon: Mail },
{ label: "Music", Icon: Music },
{ label: "Safari", Icon: Compass },
{ label: "Settings", Icon: Settings },
];
export default function DockDemo() {
return (
<Dock>
{apps.map(({ label, Icon }) => (
<DockItem key={label} label={label}>
<Icon className="size-1/2" strokeWidth={1.6} />
</DockItem>
))}
</Dock>
);
}Installation
1. Install the dependencies.
terminal npm install motion clsx tailwind-merge2. Add the
cnhelper, if you don't already have it.lib/utils.ts import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }3. Copy the component into your project.
components/ui/dock.tsx "use client"; import { motion, useMotionValue, useSpring, useTransform, type MotionValue, } from "motion/react"; import { Children, cloneElement, isValidElement, useRef, type ReactElement, type ReactNode, } from "react"; import { cn } from "@/lib/utils"; type DockProps = { children: ReactNode; className?: string; /** Peak size of a hovered item, in pixels. */ magnification?: number; /** How far from the cursor the magnification falls off, in pixels. */ distance?: number; /** Resting size of an item, in pixels. */ baseSize?: number; }; export function Dock({ children, className, magnification = 68, distance = 130, baseSize = 44, }: DockProps) { const mouseX = useMotionValue(Number.POSITIVE_INFINITY); return ( <motion.div onMouseMove={(event) => mouseX.set(event.clientX)} onMouseLeave={() => mouseX.set(Number.POSITIVE_INFINITY)} className={cn( "mx-auto flex h-16 items-end gap-3 rounded-2xl border border-line/80 bg-surface/70 px-3 pb-2.5 backdrop-blur-xl", "shadow-[0_16px_40px_-18px_rgb(0_0_0/0.45)]", className, )} > {Children.map(children, (child) => isValidElement<DockItemProps>(child) ? cloneElement(child as ReactElement<DockItemProps>, { mouseX, magnification, distance, baseSize, }) : child, )} </motion.div> ); } type DockItemProps = { children: ReactNode; label?: string; onClick?: () => void; className?: string; mouseX?: MotionValue<number>; magnification?: number; distance?: number; baseSize?: number; }; export function DockItem({ children, label, onClick, className, mouseX, magnification = 68, distance = 130, baseSize = 44, }: DockItemProps) { const ref = useRef<HTMLButtonElement>(null); const fallback = useMotionValue(Number.POSITIVE_INFINITY); const pointerX = mouseX ?? fallback; const distanceFromCenter = useTransform(pointerX, (value: number) => { const bounds = ref.current?.getBoundingClientRect(); if (!bounds) return Number.POSITIVE_INFINITY; return value - bounds.x - bounds.width / 2; }); const targetSize = useTransform( distanceFromCenter, [-distance, 0, distance], [baseSize, magnification, baseSize], ); const size = useSpring(targetSize, { mass: 0.1, stiffness: 170, damping: 14, }); return ( <motion.button ref={ref} type="button" onClick={onClick} aria-label={label} style={{ width: size, height: size }} className={cn( "group relative grid shrink-0 place-items-center rounded-xl border border-line bg-elevated text-muted", "transition-colors hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent", className, )} > {label ? ( <span className={cn( "pointer-events-none absolute -top-9 rounded-md border border-line bg-surface px-2 py-1 text-[11px] font-medium text-ink", "opacity-0 transition-opacity duration-150 group-hover:opacity-100", )} > {label} </span> ) : null} {children} </motion.button> ); }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | DockItem elements. |
| magnification | number | 68 | Peak size of a hovered item, in pixels. |
| distance | number | 130 | Falloff distance from the cursor, in pixels. |
| baseSize | number | 44 | Resting size of an item, in pixels. |
| className | string | — | Merged onto the dock container. |