Proximity, not just hover

Dev

I came across this tweet of a dock where the icons swelled based on how close the cursor was and I wanted to take that proximity idea further.

It's a small yet powerful distinction: hover is binary, you're either on an element or you're not. Proximity is continuous, as the cursor gets close, nearby elements can subtly scale and darken based on distance, which makes an interface feel responsive and alive.

I started where the tweet did, with a dock. Run your cursor across it and every tile reacts to how near you are.

Loading demo...
Tiles scale and darken by horizontal distance to the cursor. Drag the sliders to feel the falloff.

The core is a falloff function of distance in and 0 to 1 weight out. Map that weight onto whatever you like, whether that’s scale, opacity, or colour.

dock.js
onpointermove = (e) => {
  document.querySelectorAll(".dock > *").forEach((el) => {
    const r = el.getBoundingClientRect();
    const d = Math.abs(e.clientX - r.x - r.width / 2);
    const t = Math.max(0, 1 - d / 120); // 1 = on it, 0 = far away
    el.style.scale = 1 + t * 0.5;
  });
};

That snippet calls getBoundingClientRect() for every element on every pointer move, a layout read per tile, per frame. The fix is to measure once, cache the rectangles in an array, and only re-measure when the window resizes. Each frame then reads a single rect and runs pure math off the cache.

dock-cached.js
const dock = document.querySelector(".dock");
let rects = [];

// Measure once. Re-measure only when layout can actually change.
const measure = () => {
  const base = dock.getBoundingClientRect();
  rects = [...dock.children].map((el) => {
    const r = el.getBoundingClientRect();
    return { el, cx: r.left - base.left + r.width / 2 };
  });
};
measure();
addEventListener("resize", measure);

// Hot path: one rect read, then cached math.
dock.onpointermove = (e) => {
  const x = e.clientX - dock.getBoundingClientRect().left;
  for (const { el, cx } of rects) {
    const t = Math.max(0, 1 - Math.abs(x - cx) / 120);
    el.style.scale = 1 + t * 0.5;
  }
};

Same effect, but N reads-per-frame collapses to one. Here it is side by side: flip the strategy and watch the live counter.

Loading demo...
Per-frame vs cached rect reads, sampled live. Same dock, very different cost.

A few more places it shines

Once you've got a falloff weight, you can spend it on anything. Here are three variations:

Radial fields. Drop the horizontal-only constraint and use true 2D distance, so everything near the cursor responds, not just a row.

Loading demo...
2D radial proximity: a field of dots that swells toward the cursor.

Magnetism. Spend the weight on position instead of size and elements drift toward the cursor as it nears. Lovely on nav items and primary actions.

Loading demo...
Elements translate toward the cursor, capped by the pull distance.

Light. Or spend it on luminance: a soft pool that follows the cursor while nearby cards lift and their edges catch the light.

Loading demo...
A proximity-driven spotlight pooling over a card grid.

One distance to weight function, cached rects, and direct style writes get you interfaces that feel alive without costing you frames.

Posted on 31 May 2026
Permalink, All Playground