Magnify Dock
App dock where icons grow smoothly as the cursor approaches.
components/ui/magnify-dock.tsx
"use client";
import { motion, useMotionValue, useSpring, useTransform, type MotionValue } from "motion/react";
import { Calendar, Camera, Folder, Mail, MessageCircle, Music, Settings } from "lucide-react";
import { useRef, type ComponentType } from "react";
type DockItem = { label: string; icon: ComponentType<{ className?: string }>; color: string };
const items: DockItem[] = [
{ label: "Finder", icon: Folder, color: "from-sky-400 to-blue-600" },
{ label: "Mail", icon: Mail, color: "from-blue-400 to-indigo-600" },
{ label: "Messages", icon: MessageCircle, color: "from-emerald-400 to-green-600" },
{ label: "Calendar", icon: Calendar, color: "from-rose-400 to-red-600" },
{ label: "Photos", icon: Camera, color: "from-amber-400 to-orange-600" },
{ label: "Music", icon: Music, color: "from-pink-400 to-fuchsia-600" },
{ label: "Settings", icon: Settings, color: "from-zinc-400 to-zinc-600" },
];
function DockIcon({ item, mouseX }: { item: DockItem; mouseX: MotionValue<number> }) {
const ref = useRef<HTMLButtonElement>(null);
const distance = useTransform(mouseX, (x) => {
const rect = ref.current?.getBoundingClientRect();
if (!rect) return Infinity;
return x - (rect.left + rect.width / 2);
});
const size = useSpring(useTransform(distance, [-140, 0, 140], [44, 72, 44]), { stiffness: 300, damping: 22, mass: 0.2 });
const Icon = item.icon;
return (
<motion.button
ref={ref}
aria-label={item.label}
style={{ width: size, height: size }}
whileTap={{ scale: 0.9 }}
className={`group relative grid shrink-0 place-items-center rounded-2xl bg-gradient-to-br text-white shadow-md ${item.color}`}
>
<Icon className="size-1/2" />
<span className="pointer-events-none absolute -top-9 whitespace-nowrap rounded-md bg-zinc-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100">
{item.label}
</span>
</motion.button>
);
}
export function MagnifyDock({ apps = items }: { apps?: DockItem[] }) {
const mouseX = useMotionValue(Infinity);
return (
<div
onMouseMove={(e) => mouseX.set(e.clientX)}
onMouseLeave={() => mouseX.set(Infinity)}
className="flex h-[88px] items-end gap-3 rounded-3xl border border-zinc-200 bg-white/80 px-4 pb-3 shadow-xl shadow-zinc-900/10 backdrop-blur-md"
>
{apps.map((item) => (
<DockIcon key={item.label} item={item} mouseX={mouseX} />
))}
</div>
);
}
export default function Demo() {
return (
<div className="grid place-items-center pt-10">
<MagnifyDock />
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/magnify-dock.tsxcomponents/ui/magnify-dock.tsx"use client"; import { motion, useMotionValue, useSpring, useTransform, type MotionValue } from "motion/react"; import { Calendar, Camera, Folder, Mail, MessageCircle, Music, Settings } from "lucide-react"; import { useRef, type ComponentType } from "react"; type DockItem = { label: string; icon: ComponentType<{ className?: string }>; color: string }; const items: DockItem[] = [ { label: "Finder", icon: Folder, color: "from-sky-400 to-blue-600" }, { label: "Mail", icon: Mail, color: "from-blue-400 to-indigo-600" }, { label: "Messages", icon: MessageCircle, color: "from-emerald-400 to-green-600" }, { label: "Calendar", icon: Calendar, color: "from-rose-400 to-red-600" }, { label: "Photos", icon: Camera, color: "from-amber-400 to-orange-600" }, { label: "Music", icon: Music, color: "from-pink-400 to-fuchsia-600" }, { label: "Settings", icon: Settings, color: "from-zinc-400 to-zinc-600" }, ]; function DockIcon({ item, mouseX }: { item: DockItem; mouseX: MotionValue<number> }) { const ref = useRef<HTMLButtonElement>(null); const distance = useTransform(mouseX, (x) => { const rect = ref.current?.getBoundingClientRect(); if (!rect) return Infinity; return x - (rect.left + rect.width / 2); }); const size = useSpring(useTransform(distance, [-140, 0, 140], [44, 72, 44]), { stiffness: 300, damping: 22, mass: 0.2 }); const Icon = item.icon; return ( <motion.button ref={ref} aria-label={item.label} style={{ width: size, height: size }} whileTap={{ scale: 0.9 }} className={`group relative grid shrink-0 place-items-center rounded-2xl bg-gradient-to-br text-white shadow-md ${item.color}`} > <Icon className="size-1/2" /> <span className="pointer-events-none absolute -top-9 whitespace-nowrap rounded-md bg-zinc-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100"> {item.label} </span> </motion.button> ); } export function MagnifyDock({ apps = items }: { apps?: DockItem[] }) { const mouseX = useMotionValue(Infinity); return ( <div onMouseMove={(e) => mouseX.set(e.clientX)} onMouseLeave={() => mouseX.set(Infinity)} className="flex h-[88px] items-end gap-3 rounded-3xl border border-zinc-200 bg-white/80 px-4 pb-3 shadow-xl shadow-zinc-900/10 backdrop-blur-md" > {apps.map((item) => ( <DockIcon key={item.label} item={item} mouseX={mouseX} /> ))} </div> ); } export default function Demo() { return ( <div className="grid place-items-center pt-10"> <MagnifyDock /> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.