Magnetic Dock

An interactive interface element that responds to mouse proximity and uses spring physics to scale icons and bounce them upward when hovered over. It includes glassmorphism, soft gradients, and tooltip overlays for a smooth and responsive user experience.

Live Preview

Magnetic Dock

Home
🏠
Vault
🔒
Archive
📂
Signal
📶
Settings
⚙️

Dependencies

npm installframer-motion

Code

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

const ITEMS = [
  {
    label: "Home",
    color:
      "shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] from-sky-400/20 to-blue-600/5 border-sky-400/20 text-sky-400",
    letter: "🏠",
  },
  {
    label: "Vault",
    color:
      "shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] from-violet-400/20 to-fuchsia-600/5 border-violet-400/20 text-violet-400",
    letter: "🔒",
  },
  {
    label: "Archive",
    color:
      "shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] from-emerald-400/20 to-teal-600/5 border-emerald-400/20 text-emerald-400",
    letter: "📂",
  },
  {
    label: "Signal",
    color:
      "shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] from-amber-400/20 to-orange-600/5 border-amber-400/20 text-amber-400",
    letter: "📶",
  },
  {
    label: "Settings",
    color:
      "shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] from-zinc-300/15 to-zinc-500/5 border-zinc-500/20 text-zinc-300",
    letter: "⚙️",
  },
];

interface DockItemProps {
  mouseX: MotionValue;
  item: (typeof ITEMS)[number];
}

function DockItem({ mouseX, item }: DockItemProps) {
  const ref = useRef<HTMLDivElement>(null);

  const distance = useTransform(mouseX, (val) => {
    const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
    return val - bounds.x - bounds.width / 2;
  });

  const sizeSync = useTransform(distance, [-120, 0, 120], [52, 96, 52]);
  const size = useSpring(sizeSync, {
    stiffness: 300,
    damping: 20,
    mass: 0.5,
  });

  const ySync = useTransform(distance, [-120, 0, 120], [0, -22, 0]);
  const y = useSpring(ySync, {
    stiffness: 300,
    damping: 20,
    mass: 0.5,
  });

  return (
    <div className="relative group flex items-end">
      <div className="absolute -top-12 left-1/2 -translate-x-1/2 px-2.5 py-1 bg-zinc-900 border border-zinc-800 rounded-lg text-[11px] text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none font-mono tracking-wider shadow-2xl">
        {item.label}
      </div>

      <motion.div
        ref={ref}
        style={{ width: size, height: size, y }}
        className={`flex items-center justify-center rounded-2xl bg-gradient-to-br ${item.color} shadow-[0_8px_30px_rgb(0,0,0,0.3)] border border-white/20 backdrop-blur-md cursor-pointer origin-bottom`}
      >
        <span className="text-white text-xl font-bold select-none drop-shadow-md">
          {item.letter}
        </span>
      </motion.div>
    </div>
  );
}

export default function MagneticDock() {
  const mouseX = useMotionValue(Infinity);

  return (
    <div className="flex flex-col items-center justify-center h-full p-8 w-full">
      <h2 className="text-zinc-500 font-mono text-xs mb-12 uppercase tracking-widest text-center">
        Magnetic Dock
      </h2>

      <div className="relative p-2">
        <div className="absolute inset-0 bg-zinc-900/60 backdrop-blur-2xl border border-white/10 rounded-[32px] shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5)]" />

        <motion.div
          onMouseMove={(e) => mouseX.set(e.pageX)}
          onMouseLeave={() => mouseX.set(Infinity)}
          className="relative flex h-20 items-end gap-3.5 px-6 py-4"
        >
          {ITEMS.map((item, i) => (
            <DockItem key={i} mouseX={mouseX} item={item} />
          ))}
        </motion.div>
      </div>
    </div>
  );
}

Props

PropTypeDefaultDescription
itemsItem[]ITEMSAn array of objects containing the label, Tailwind CSS classes for color/styling, and an emoji/character icon for each element in the dock.