---
title: "Interfaces that move before you do"
description: "Most UIs wait for a click. The good ones react to intent — a focus ring that warms as your cursor approaches, a button that leans into you, a nav item armed before you arrive. The math is a point-to-rectangle distance and a falloff curve, and it's a few lines of pointermove. Plus where it earns its keep and where it quietly breaks accessibility."
date: "2026-06-13"
tags: ["frontend", "interaction-design", "css", "javascript"]
---

Most interfaces are reactive: you click, they respond. The nicer ones are
*anticipatory* — they react to where your attention is going, not just where it
landed. [Gabriel](https://x.com/gabriell_lab/status/2065039972483182708) calls
this *input anticipation*, and the demo that made it click for me is almost
embarrassingly small: a focus ring that fades in as your cursor gets close to a
field, so the input feels alive before you've touched it.

Everything below runs live. Move your cursor toward the chat box.

<ProximityRing />

## It's all one distance

The interesting part isn't the glow, it's *how far the cursor is from the
element* — and "distance to a rectangle" is sneakier than "distance to a point".
You don't want the distance to the field's center; you want the distance to its
nearest edge, which should read as *zero* the moment the cursor is anywhere
over the field. That clamp is the whole idea:

```js
const r = box.getBoundingClientRect();
const dx = Math.max(r.left - x, 0, x - r.right);
const dy = Math.max(r.top - y, 0, y - r.bottom);
const distance = Math.hypot(dx, dy);
```

`Math.max(left - x, 0, x - right)` is the trick. If the cursor is left of the
box, `left - x` wins. If it's to the right, `x - right` wins. If it's *inside*
the box's horizontal span, both are negative and the `0` wins — no horizontal
distance. Same for `dy`. The result is the gap to the nearest edge, and it's `0`
anywhere over the element.

Then you turn distance into intent with a falloff:

```js
const intent = Math.max(0, 1 - distance / radius) ** exponent;
ring.style.opacity = intent;
```

`radius` is how far away the field starts noticing you; `exponent` shapes the
curve. Drag the sliders under the demo — `^1` is a flat linear ramp that feels
mushy, and `^2` (Gabriel's value) keeps the ring quiet until you're committed,
then snaps it on. The squaring is doing the design work: it makes the response
*non-linear* so faraway movement is ignored and nearby movement is amplified.
That's the difference between "a glow that follows my mouse everywhere" and "the
field noticed I'm coming."

One implementation note the demo follows: don't drive this through React state.
A `pointermove` fires dozens of times a second, and re-rendering on each one
will cost you frames. Read `getBoundingClientRect()` and write
`ring.style.opacity` straight to the node. State is for the sliders, which change
rarely; the hot path touches the DOM directly.

## The target can come to you

Once you're reading proximity, you can do more than light something up — you can
*move* it. A magnetic target leans toward the cursor as you approach, so the
click region effectively grows in the direction you're already traveling. It's
the same proximity value, applied to a `transform` instead of an `opacity`:

<MagneticButton />

The pull is `1 - dist / range`, the offset is `direction × max × pull`, and the
spring back uses an overshoot easing (`cubic-bezier(0.34, 1.56, 0.64, 1)`) so it
feels like a physical thing settling, not a CSS property relaxing. Keep `max`
small — 10–15px. Past that the button feels like it's dodging you, which is the
opposite of helpful.

## Predicting the destination

The strongest version of anticipation isn't proximity at all — it's
*trajectory*. Where is the cursor actually heading? If you track velocity, you
can guess the target a beat before arrival and arm it: highlight it, warm it up,
or kick off the fetch its page will need. Move across this row and watch the
prediction lock on, then "prefetch" once it holds:

<IntentPrediction />

The heuristic is cheap: keep an exponential moving average of pointer velocity,
normalize it, and for each target take the dot product of the heading with the
direction to that target. A dot product near `1` means "you're aimed straight at
this one." Require a forward cone (ignore anything you're not actually moving
toward), break ties by distance, and when the same target stays predicted for a
breath, treat that as confidence and fire the prefetch. Slow down or stop and it
falls back to nearest-target, because a still cursor has no heading to read.

This is the genuinely useful version of anticipation: doing the expensive work
in the ~200ms between intent and click. Submenus that open on the path to them,
links that prefetch when you're clearly headed there, a search index that warms
as you drift toward the box. I took this exact idea and built it into a
prefetching library — [intently](/writing/prefetching-on-intent) — that loads
the page you're aimed at before you click.

## Where it earns its keep — and where it doesn't

I'm going to be honest about this one, because it's easy to over-apply.

It's not free. Every anticipating element is doing geometry on every
`pointermove`. One AI chat field? Imperceptible. Two hundred buttons each running
their own distance math? You've built a space heater. Gabriel's own rule is the
right one: reserve it for the few high-value targets — the chat box, the primary
CTA, a nav you want to feel fast — not every interactive thing on the page.

It's pointer-only, which is an accessibility trap. Someone asked Gabriel
about a11y and the honest answer is: keyboard users, touch users, and screen
reader users get *none* of this, because there's no cursor to anticipate. That's
fine *as long as the effect is pure decoration* — the field still focuses, the
button still clicks, the link still works. The failure mode is letting
anticipation become *function*: if a menu only opens because you predicted it,
or a control is only reachable by sweeping a mouse near it, you've locked out
everyone not driving with a mouse. Anticipation should make the fast path feel
faster, never be the only path.

Respect `prefers-reduced-motion`. The proximity ring is a fade, which is
gentle, but the magnetic pull is motion — freeze the transform for anyone who's
asked the OS to calm things down. (The magnetic demo above does.)

The mental model that stuck with me: a reactive interface answers questions; an
anticipatory one notices you walking up. Used on the one or two elements that
deserve it, it's the cheapest "this feels expensive" win I know — a few lines of
`pointermove`, a clamp, and a curve.

---

I packaged the patterns here — proximity ring, magnetic target, and trajectory
prediction — into a copy-paste Claude Code skill so your agent can drop them
into a real app (and knows the perf and a11y rules above). There's a
[dedicated page with a one-command install](/freebies/input-anticipation), and it
lives alongside the rest of my skills in
[github.com/seangeng/skills](https://github.com/seangeng/skills). Credit for the
original tip goes to [Gabriel](https://x.com/gabriell_lab/status/2065039972483182708).
