Proximity
I came across this tweet of a dock where the icons get bigger the closer the cursor is, and I wanted to take that idea a bit further.
The difference with hover is that it's basically on or off, you're either over something or you're not. With proximity it's gradual, so as the cursor gets closer, the elements near it scale up and get a bit darker depending on the distance. I think it makes an interface feel a lot more responsive.
I started where the tweet did, with a dock. If you move your cursor across it, each tile changes size depending on how close you are.
The main bit is a falloff function, which takes a distance and gives back a weight between 0 and 1. From there I can map that weight onto pretty much anything, like scale or opacity or colour.
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, which is a layout read per tile, every frame. So I measure everything once, cache the rectangles in an array, and only measure again when the window gets resized. Each frame then just reads one rect and does the maths off the cache.
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;
}
};It looks exactly the same, but it goes from a read per tile every frame down to one. Here they are side by side, so you can switch between them and watch the live counter.
Variations
Once I had the falloff weight I could use it for pretty much anything, so here are a few variations.
Radial fields. I dropped the horizontal-only bit and used proper 2D distance, so everything in a circle around the cursor changes.
Magnetism. If I use the weight on position instead, elements move towards the cursor as it gets close. I really like this on nav items and main buttons.
Light. Using it on brightness gives a soft circle of light around the cursor, and the cards near it move up slightly and their edges get lighter.
So it's really just one function that turns distance into a weight, and then caching the rects and writing the styles directly. It barely costs anything per frame, which I was pretty happy with.
Posted on 31 May 2026
Permalink, All Playground