Recreation of the magnification effect from the Mac OS dock.
Uses Framer Motion to track the mouse position in a Motion Value called mouseX
. Each icon then derives the pixel distance from the mouse position to its center, and uses a transform to adjust its width based on that distance.
Infinity is used as the null value for mouseX
to keep the transforms clean – no active/inactive state is needed to enable magnification since Infinity always falls outside the domain and thus resets the width of each icon.
Note that this demo is not identical to the actual Mac OS dock, since the width of the dock on the far side of the mouse doesn't move with the icons. This behavior is left as an exercise for the reader – or for a future recipe!
Code
import {
MotionValue,
motion,
useMotionValue,
useSpring,
useTransform,
} from "framer-motion";
import { useRef } from "react";
function Dock() {
let mouseX = useMotionValue(Infinity);
return (
<motion.div
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
className="mx-auto flex h-16 items-end gap-4 rounded-2xl bg-gray-700 px-4 pb-3"
>
{[...Array(8).keys()].map((i) => (
<AppIcon mouseX={mouseX} key={i} />
))}
</motion.div>
);
}
function AppIcon({ mouseX }: { mouseX: MotionValue }) {
let ref = useRef<HTMLDivElement>(null);
let distance = useTransform(mouseX, (val) => {
let bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
return val - bounds.x - bounds.width / 2;
});
let widthSync = useTransform(distance, [-150, 0, 150], [40, 100, 40]);
let width = useSpring(widthSync, { mass: 0.1, stiffness: 150, damping: 12 });
return (
<motion.div
ref={ref}
style={{ width }}
className="aspect-square w-10 rounded-full bg-gray-400"
/>
);
}