Dock Menu

macOS-style dock with spring magnification on hover

navigationdockmenumacosinteractive
Install dependencies
$npm install framer-motion
Preview

Source Code

"use client";

import { useRef } from "react";
import {
  motion,
  useMotionValue,
  useSpring,
  useTransform,
} from "framer-motion";

interface DockItem {
  icon: React.ReactNode;
  label: string;
  onClick?: () => void;
}

interface DockMenuProps {
  items: DockItem[];
  baseSize?: number;
  maxSize?: number;
  distance?: number;
  className?: string;
}

function DockIcon({
  item,
  mouseX,
  baseSize,
  maxSize,
  distance,
}: {
  item: DockItem;
  mouseX: import("framer-motion").MotionValue<number>;
  baseSize: number;
  maxSize: number;
  distance: number;
}) {
  const ref = useRef<HTMLButtonElement>(null);

  const distFromMouse = useTransform(mouseX, (val) => {
    if (!ref.current || val < 0) return distance + 1;
    const rect = ref.current.getBoundingClientRect();
    const center = rect.left + rect.width / 2;
    return Math.abs(val - center);
  });

  const size = useSpring(
    useTransform(distFromMouse, [0, distance], [maxSize, baseSize], {
      clamp: true,
    }),
    { damping: 20, stiffness: 200, mass: 0.5 }
  );

  return (
    <motion.button
      ref={ref}
      onClick={item.onClick}
      className="group relative flex flex-col items-center"
      style={{ width: size, height: size }}
      whileTap={{ scale: 0.9 }}
    >
      <motion.div
        className="flex h-full w-full items-center justify-center rounded-2xl border border-white/15 bg-gray-800/80 backdrop-blur-md transition-colors group-hover:bg-gray-700/80"
        style={{ width: size, height: size }}
      >
        {item.icon}
      </motion.div>
      <span className="absolute -bottom-6 whitespace-nowrap rounded-md bg-gray-900 px-2 py-0.5 text-xs text-white opacity-0 shadow-lg transition-opacity group-hover:opacity-100">
        {item.label}
      </span>
    </motion.button>
  );
}

export default function DockMenu({
  items,
  baseSize = 48,
  maxSize = 72,
  distance = 140,
  className = "",
}: DockMenuProps) {
  const mouseX = useMotionValue(-1000);

  return (
    <motion.div
      onMouseMove={(e) => mouseX.set(e.clientX)}
      onMouseLeave={() => mouseX.set(-1000)}
      className={`flex items-end gap-2 rounded-2xl border border-white/15 bg-gray-900/60 px-3 py-2 backdrop-blur-xl ${className}`}
    >
      {items.map((item, i) => (
        <DockIcon
          key={i}
          item={item}
          mouseX={mouseX}
          baseSize={baseSize}
          maxSize={maxSize}
          distance={distance}
        />
      ))}
    </motion.div>
  );
}