Skip to content
← writing

Interfaces that move before you do

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 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.

intent: 0.00

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:

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:

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:

The button leans into the cursor before you arrive — the click target comes to you.

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:

sweep toward a target — the one you're aimed at lights up before you reach it

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 — 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, and it lives alongside the rest of my skills in github.com/seangeng/skills. Credit for the original tip goes to Gabriel.

Ask your agent to implement this

Read the full writeup at https://seangeng.com/writing/interfaces-that-anticipate-input.md and implement it in my project.

It covers: Interfaces that move before you do — 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.

Requirements:
- Follow the technique/approach exactly as described in the writeup.
- Adapt names, colors, and styling to my project's existing conventions.
- If it's a component, make it reusable with sensible props and TypeScript types.
- Keep it accessible: semantic HTML, keyboard support, and respect prefers-reduced-motion.
- When done, tell me which files you created or changed and how to use it.

Paste into Claude Code, Codex, Cursor, or any agent. view raw .md