Dock Menu
macOS-style dock with spring magnification on hover
navigationdockmenumacosinteractive
Install dependencies
$npm install framer-motionPreview
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>
);
}