Proximity

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, distance in and 0 to 1 weight out. From there I can map that weight onto whatever I like, 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. So I measure once instead, cache the rectangles in an array, and only re-measure when the window resizes. Each frame then reads a single rect and runs pure maths 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, and N reads per frame collapses to one. Here they are 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 things to do with it

Once I had the falloff weight I could spend it on anything. Here are three variations.

Radial fields. I dropped the horizontal-only constraint and used true 2D distance, so the whole field around the cursor responds.

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

Magnetism. Spending the weight on position instead makes elements drift toward the cursor as it gets near. Lovely on nav items and primary actions.

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

Light. Spending it on luminance gives a soft pool that follows the cursor, with nearby cards lifting and their edges catching it.

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

One distance-to-weight function, cached rects, and direct style writes. That's all of it, and it costs almost nothing per frame.

Posted on 31 May 2026
Permalink, All Playground