Retour au catalogue

Sidebar Floating Dock

Dock flottant style macOS avec effet de grossissement au survol et animations fluides.

sidebarmedium Both Responsive a11y
boldelegantsaassaasportfolioasymmetric
Theme

How to build a magnifying dock navbar in React

A macOS-style dock in React tracks mouse position with Framer Motion's useMotionValue, computes the distance from each icon's center, and feeds that distance into a useSpring-backed useTransform to scale the icon size in real time. The spring parameters (mass, stiffness, damping) control how elastic the magnification feels.

  • Stack: React + Framer Motion 11 + Lucide React + Tailwind v4, ~225 lines, zero extra dependencies.
  • Core Framer Motion APIs: useMotionValue, useSpring, useTransform, AnimatePresence.
  • Supports both horizontal (bottom dock) and vertical (side dock) orientations via a prop.
  • Active item uses layoutId for a shared-layout dot indicator that slides between items.
  • Accessible prop set (aria) is not yet wired; tooltips degrade gracefully without JS.

This floating dock brings the macOS application shelf to the browser. Each icon senses the cursor's proximity and grows smoothly as the pointer approaches, shrinks as it moves away. The whole bar enters with a spring slide-up, tooltip labels fade in on hover and a shared-layout dot slides between the active item. It works as a bottom navigation bar, a side toolbar or a command palette launcher.

Anatomy

The root wrapper captures onMouseMove and writes clientX/clientY to two top-level motion values. The dock container is a flex row (or column) rounded pill with a glass-morphism background. Each icon slot is a DockIcon component holding a button, a notification badge, an active dot and a tooltip. A visual separator and a fixed Plus button sit at the end of the list, separated from the dynamic items by a 1px rule.

How it works

Inside each DockIcon, useTransform reads the shared mouseX (or mouseY for vertical) motion value and computes the distance from the icon's center via getBoundingClientRect. That raw distance feeds a useTransform with a distance-to-size mapping ([0, 100, 200] → [56, 48, 44]px). A useSpring wraps the output with mass 0.1, stiffness 200 and damping 15, giving the magnification a snappy but not jarring feel. The result is applied to width and height on the motion.button, so the icon scales from its center without layout shift.

How to build it in React

  1. Create shared mouse motion values at the dock level

    At the top of the dock component, call useMotionValue(0) for both X and Y. Attach an onMouseMove handler to the root div that calls mouseX.set(e.clientX) and mouseY.set(e.clientY). Pass both values down to each icon as props.

    const mouseX = useMotionValue(0);
    const mouseY = useMotionValue(0);
    
    <div onMouseMove={(e) => { mouseX.set(e.clientX); mouseY.set(e.clientY); }}>
  2. Compute per-icon distance from the pointer

    Inside DockIcon, attach a ref to the button. Use useTransform on mouseX (or mouseY) with a callback that reads getBoundingClientRect() to get the icon center, then returns Math.abs(pointerPosition - center). This produces a live distance value that updates every frame the pointer moves.

    const distance = useTransform(mouseX, (val) => {
      const bounds = ref.current?.getBoundingClientRect();
      if (!bounds) return 200;
      return Math.abs(val - (bounds.x + bounds.width / 2));
    });
  3. Map distance to size with a spring

    Use useTransform on distance with an input range [0, 100, 200] and an output range [56, 48, 44]. Wrap that in useSpring with low mass and moderate stiffness so the icon settles quickly without bouncing. Apply the resulting motion value to width and height on your motion.button.

    const size = useSpring(
      useTransform(distance, [0, 100, 200], [56, 48, 44]),
      { mass: 0.1, stiffness: 200, damping: 15 }
    );
    
    <motion.button style={{ width: size, height: size }} />
  4. Add tooltip and active indicator

    Wrap the tooltip in AnimatePresence and render it only when isHovered is true. Use initial/animate/exit on a motion.div for a quick fade-scale. For the active indicator, give all instances the same layoutId ('dock-active') so Framer Motion automatically animates the dot sliding from one item to the next.

    <AnimatePresence>
      {isHovered && (
        <motion.div
          initial={{ opacity: 0, y: 4, scale: 0.9 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          exit={{ opacity: 0, y: 4, scale: 0.9 }}
        >
          {item.label}
        </motion.div>
      )}
    </AnimatePresence>
    
    {item.active && (
      <motion.div layoutId="dock-active" className="w-1 h-1 rounded-full" />
    )}

When to use it

Reach for this dock in app shells where you want a compact, always-visible navigation that does not claim a full sidebar column. It suits SaaS dashboards, creative tools, portfolio sites and any context where six to ten top-level destinations need quick access. Avoid it on content-heavy pages where a persistent sidebar with labels improves scanability, and skip the magnification effect on touch devices since proximity sensing requires a real pointer.

Used by

  • Apple macOS, The original magnifying dock that inspired this pattern, used as the primary app launcher on every Mac.
  • Notion, Uses a compact floating action toolbar in its canvas views that echoes the dock metaphor.
  • Framer, The Framer canvas editor exposes a floating toolbar with icon-based tools that scale and highlight on hover.
  • Linear, Command bar and quick-action palette use compact icon-first navigation with animated selection indicators.

FAQ

Why does the magnification use getBoundingClientRect instead of a position prop?

getBoundingClientRect gives the icon's actual rendered position in viewport coordinates, which matches the clientX/clientY values from the mouse event. Passing a pre-calculated position prop would break if the dock shifts layout, gets scrolled or is repositioned dynamically.

How do I make the dock stick to the bottom of the screen?

Wrap the dock in a fixed container: `position: fixed; bottom: 1.5rem; left: 50%; transform: translateX(-50%)`. Move the onMouseMove listener to the window instead of the dock div so proximity sensing works even when the cursor is above the dock.

Can I use the dock vertically?

Yes. Pass orientation='vertical' to switch the flex direction to column and change the proximity calculation from mouseX to mouseY. The separator also rotates from a vertical rule to a horizontal one automatically via the conditional className.

Does the shared-layout active dot animate between items?

Yes, as long as only one item has active: true at a time. Framer Motion's layoutId='dock-active' tracks the single mounted instance and smoothly repositions it when active switches to a different item, no manual animation code needed.

"use client";

import { useState, useRef } from "react";
import {
  motion,
  useMotionValue,
  useSpring,
  useTransform,
  AnimatePresence,
  type MotionValue,
} from "framer-motion";
import {
  Home,
  Search,
  FolderOpen,
  MessageSquare,
  Calendar,
  Settings,
  User,
  Plus,
} from "lucide-react";

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reviews

React macOS Dock Component with Framer Motion Magnification