How to build a macOS-style dock navbar in React with Framer Motion
A macOS-style dock navbar in React uses Framer Motion's useMotionValue to track the cursor's X position and useTransform to map the distance between the cursor and each icon to a size value, then smooths the result with useSpring. Icons closest to the pointer grow the most, creating the characteristic wave magnification.
- Stack: React 19, Framer Motion 11, Lucide React, Tailwind v4 CSS tokens, ~137 lines.
- Core API: useMotionValue, useTransform, useSpring with stiffness 300, damping 25, mass 0.5.
- Icons animate from 44px (BASE_SIZE) to 68px (MAX_SIZE) within a 140px influence radius.
- Accessible: every icon has aria-label and a CSS tooltip; hover state changes background to --color-accent.
- Touch caveat: the magnification depends on pointer position, so it degrades gracefully to static icons on mobile.
The Navbar Dock is a fixed bottom navigation bar that mimics the macOS Dock. Each icon grows as the cursor approaches it and its neighbors follow with decreasing intensity, producing a fluid wave effect. It replaces the top navbar pattern entirely and works especially well on portfolio, one-page, and tool-focused sites where navigation is secondary to content.
Anatomy
The component is a fixed `<nav>` centered horizontally at the bottom of the viewport with a frosted glass background (backdrop-filter blur + color-mix transparency). Inside, a row of `DockIcon` components each renders a `motion.a` sized by a spring motion value. A tooltip `<span>` floats above each icon and appears on :hover via a CSS rule injected inline. The nav container tracks pointer X via onMouseMove and resets to -999 on leave, which collapses all icons to their base size.
How it works
A single `useMotionValue(-999)` lives in the parent `NavbarDock` component and is passed down to every `DockIcon`. In each icon, `useTransform` reads the shared mouseX value and computes the pixel distance between the cursor and that icon's center by calling `getBoundingClientRect()` inside the transform callback. That distance is then mapped linearly from 0 to 140px onto the size range 68px to 44px, giving closer icons a larger value. The raw size feeds into `useSpring` (stiffness 300, damping 25, mass 0.5) to produce smooth, springy resize without jank. When mouseX is -999 (cursor outside the dock), every distance computation returns DISTANCE (140), so all icons collapse to BASE_SIZE.
How to build it in React
Create the shared mouse motion value
In the parent component, declare a single `useMotionValue(-999)` and wire it to `onMouseMove` and `onMouseLeave` on the nav element. The -999 sentinel signals that the cursor is outside the dock.
const mouseX = useMotionValue(-999); <nav onMouseMove={(e) => mouseX.set(e.clientX)} onMouseLeave={() => mouseX.set(-999)} >Compute distance from cursor to icon center
In each `DockIcon`, attach a ref to the anchor element. Pass the shared mouseX into `useTransform` and read the icon's bounding rect inside the transform callback to get the cursor-to-center distance in real time.
const distance = useTransform(mouseX, (val: number) => { const el = ref.current; if (!el || val === -999) return DISTANCE; // collapse const rect = el.getBoundingClientRect(); const center = rect.left + rect.width / 2; return Math.abs(val - center); });Map distance to size with a spring
Use a second `useTransform` to map the distance range [0, DISTANCE] to [MAX_SIZE, BASE_SIZE]. Wrap the result in `useSpring` so the size change is smooth and physically weighted rather than instant.
const sizeRaw = useTransform(distance, [0, DISTANCE], [MAX_SIZE, BASE_SIZE]); const size = useSpring(sizeRaw, { stiffness: 300, damping: 25, mass: 0.5 }); // Apply to the motion element <motion.a style={{ width: size, height: size }} ... />Add a frosted glass container and tooltips
Style the nav with `backdrop-filter: blur(16px)` and a `color-mix(in srgb, var(--color-background) 80%, transparent)` background so it floats above content. For each icon, position a `<span>` above with `opacity: 0` and reveal it on anchor :hover via an inline `<style>` block.
When to use it
The dock navbar suits portfolio sites, single-page apps, design tools, and creative agencies where the navigation is compact (4-7 items) and the interaction itself becomes part of the brand feel. Skip it on content-heavy or e-commerce sites where a top navbar with dropdowns is more practical, and avoid it if your users are primarily on mobile, since the magnification effect requires a pointer.
Used by
- Apple, Originated the macOS Dock pattern in 2001; the magnification behavior is the canonical reference for this component.
- Linear, Uses a compact icon rail in its app sidebar with smooth hover transitions that echo the dock's direct-manipulation feel.
- Raycast, Its marketing site features spring-animated icon grids with magnification on hover, directly inspired by the macOS Dock.
- Framer, Showcases dock-style magnification as a built-in interaction in its visual editor, making it a staple for Framer-built sites.
FAQ
Why does each DockIcon need its own ref?
The ref gives access to the icon's bounding rect inside the useTransform callback so the component can compute the real pixel distance from the cursor to that specific icon's center. Without it, magnification could not be position-aware.
Can I use this with Next.js Link instead of plain anchors?
Yes. Replace `motion.a` with `motion(Link)` from Framer Motion's `motion()` factory and pass `href` and `ref` as usual. Make sure to set `legacyBehavior={false}` (the default in Next 13+) so Link renders a single anchor element.
How do I add more icon types beyond the built-in map?
Extend the `ICON_MAP` object with additional Lucide (or any React SVG) components keyed by a string identifier. Then pass that identifier in the `icon` field of each dock item. The component falls back to the `Home` icon for unknown keys.
Does the spring configuration affect perceived smoothness?
Strongly. Higher stiffness makes icons snap faster to their target size; lower damping adds oscillation. The defaults (stiffness 300, damping 25, mass 0.5) give a snappy but not jittery response. For a softer dock, try stiffness 150 and damping 20.