01
macOS-style dock magnification
Icons swell as the cursor passes and settle as it leaves. The whole illusion is one pointer handler and a scale — but the gap between the demo that looks right and the one that feels right is three small details.
Hover across the icons. Toggle to feel the jitter and overlap.
onpointermove = e => document.querySelectorAll(".dock>*").forEach(el => {
const r = el.getBoundingClientRect();
const t = Math.max(0, 1 - Math.abs(e.clientX - r.x - r.width / 2) / 120);
el.style.scale = 1 + t * .5;
});It reads beautifully and it half-works. Three things break the moment it meets a real dock:
- Jitter.
getBoundingClientRect()reads the already-scaled element on every move, so the measured center drifts as the item grows and the math fights itself. Cache the resting centers once — onpointerenter— and the wobble disappears. - Overlap. The default
transform-originiscenter, so items balloon in every direction and collide. A real dock grows upward from the baseline (transform-origin: bottom) and pushes its neighbours aside. Without that spread, scaled icons just sit on top of each other. - Stuck & global. Assigning to bare
onpointermovebinds the whole window, and with no reset the icons stay puffed once the pointer leaves. Scope the listener to the dock and clear every transform onpointerleave.
/* grow upward from the baseline, not outward from the center */
.dock > * {
transform-origin: bottom;
transition: transform .12s ease-out;
}const dock = document.querySelector(".dock");
const items = [...dock.children];
let centers = null; // measured once, while at rest
dock.addEventListener("pointerenter", () => {
centers = items.map(el => {
const r = el.getBoundingClientRect();
return r.x + r.width / 2; // resting center — never re-read while scaled
});
});
dock.addEventListener("pointermove", e => {
const scale = centers.map(cx =>
1 + Math.max(0, 1 - Math.abs(e.clientX - cx) / 120) * 0.6
);
items.forEach((el, i) => {
// push neighbours aside by half the extra width each one gained
let shift = 0;
centers.forEach((cx, j) => {
if (j === i) return;
const half = (scale[j] - 1) * el.offsetWidth / 2;
shift += cx < centers[i] ? half : -half;
});
el.style.transform = `translateX(${shift}px) scale(${scale[i]})`;
});
});
dock.addEventListener("pointerleave", () => {
centers = null;
items.forEach(el => (el.style.transform = "")); // un-puff
});