Design Engineering

A notebook of interesting components and how they actually work — the elegant version, and the one that ships.

01

macOS-style dock magnification

pointer eventstransformslayout math

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.

The elegant 4-line demo
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:

  1. 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 — on pointerenter — and the wobble disappears.
  2. Overlap. The default transform-origin is center, 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.
  3. Stuck & global. Assigning to bare onpointermove binds 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 on pointerleave.
The CSS that does half the work
/* grow upward from the baseline, not outward from the center */
.dock > * {
  transform-origin: bottom;
  transition: transform .12s ease-out;
}
What ships
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
});