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 install
framer-motionCode
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
| Prop | Type | Default | Description |
|---|---|---|---|
| items | Item[] | ITEMS | An array of objects containing the label, Tailwind CSS classes for color/styling, and an emoji/character icon for each element in the dock. |