Input anticipation
Claude Code skills & agent prompts I actually reach for — copy-paste, no deps. Writeups at seangeng.com.
npx -y skills add seangeng/skills --skill input-anticipationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Implement anticipatory UI — interfaces that react to pointer intent *before* the click. Three patterns: a proximity focus ring that warms as the cursor nears a field, a magnetic target that leans toward the cursor, and trajectory prediction that arms/prefetches the element the cursor is heading toward. Use this skill whenever the user wants a UI to "feel alive / responsive / fast", asks for a glow or focus ring that reacts to the mouse, a magnetic or cursor-attracting button, hover-intent or predictive prefetch, "make the chat box feel premium", or mentions input anticipation / anticipatory design / reacting to cursor proximity. Also reach for it proactively when building a hero CTA, an AI chat input, or a primary nav and the user wants polish. It bakes in the performance budget (reserve it for 1-2 high-value targets, never every button) and the accessibility rules (pointer-only effects must stay pure decoration, never become the only way to reach a control), so the result is tasteful and safe rather than a gimmick.
SKILL.md
11.5 KB, as published. Nobody here has run it
Input anticipation
Most interfaces are reactive: the user clicks, the UI responds. Anticipatory interfaces react to intent — where the pointer is going — a beat before the interaction lands. Done on the right element it reads as "this feels expensive"; done on every element it's a laggy gimmick that hurts. This skill implements the three core patterns and, just as importantly, encodes when not to use them.
The whole family is built on one cheap signal: how far is the pointer from this element, and where is it heading? Everything else is turning that signal into opacity, transform, or a prefetch.
Before you build: the two rules that keep this tasteful
These aren't footnotes — they're why the effect works at all. Read them first.
1. Reserve it for 1–2 high-value targets. Every anticipating element runs
geometry on every pointermove (dozens of events per second). One AI chat field
or one hero CTA: imperceptible. Two hundred buttons each doing their own distance
math: you've built a space heater and the page stutters. The effect also means
less when everything does it — anticipation signals "this is the thing you came
for." Apply it to the primary input/CTA/nav, not the whole page.
2. It's pointer-only, so it must be pure decoration — never function.
Keyboard users, touch users, and screen-reader users have no cursor to
anticipate, so they get none of this. That's fine as long as the underlying
control works completely without it: the field still focuses, the button still
clicks, the link still navigates. The trap is letting anticipation become the
only path — e.g. a menu that opens only on predicted approach, or a control
reachable only by sweeping a mouse near it. That locks out everyone not on a
mouse. Rule: anticipation makes the fast path feel faster; it is never the only
path. Also honor prefers-reduced-motion for anything that moves.
The shared signal: distance from a point to a rectangle
You almost never want distance to an element's center — you want distance to
its nearest edge, which should read as 0 the instant the pointer is anywhere
over the element. The clamp trick gives you exactly that:
function distanceToRect(x, y, rect) {
const dx = Math.max(rect.left - x, 0, x - rect.right);
const dy = Math.max(rect.top - y, 0, y - rect.bottom);
return Math.hypot(dx, dy);
}
Why the Math.max(a, 0, b): if the pointer is left of the box, left - x is
positive and wins; if right of it, x - right wins; if horizontally inside
the box's span, both are negative and 0 wins — no horizontal gap. Same for
dy. The result is the gap to the nearest edge, and 0 anywhere inside.
Turn distance into a 0→1 "intent" with a falloff curve:
function intentFrom(distance, radius = 180, exponent = 2) {
return Math.max(0, 1 - distance / radius) ** exponent;
}
radius is how far away the element starts noticing the pointer. exponent
shapes the response: ^1 is linear and feels mushy (the element reacts to
faraway movement); ^2 keeps it quiet until the pointer is committed, then ramps
fast. The squaring is doing the design work — it makes the response non-linear so
distant motion is ignored and near motion is amplified. Default to ^2.
Performance: never drive the hot path through framework state
A pointermove handler fires constantly. Re-rendering a component on every event
will cost frames. Read layout and write style straight to the DOM node;
reserve state for things that change rarely (slider values, enabled/disabled).
Read getBoundingClientRect() inside the handler (it reflects scroll); if you
cache it, invalidate on scroll/resize.
Pattern 1 — Proximity focus ring
A ring/glow whose opacity tracks pointer distance. The canonical use is an AI chat input that "wakes up" as you approach. Vanilla:
const field = document.querySelector(".field"); // the element to sense
const ring = document.querySelector(".ring"); // decorative overlay, aria-hidden
addEventListener("pointermove", (e) => {
const d = distanceToRect(e.clientX, e.clientY, field.getBoundingClientRect());
ring.style.opacity = intentFrom(d, 180, 2);
});
.ring {
position: absolute; inset: -4px; border-radius: inherit;
pointer-events: none; opacity: 0;
transition: opacity 90ms linear; /* smooths the per-event jumps */
box-shadow: 0 0 0 1px hsl(217 91% 60% / .6),
0 0 24px 2px hsl(217 91% 60% / .45);
}
Mark the ring aria-hidden="true" — it's pure decoration. Scope the listener to
a container instead of window when you have several on a page, and reset to 0
on pointerleave. You can drive more than opacity off the same intent: warm a
Send button's border, lift a placeholder. Keep it subtle.
React note: keep intent out of state. Hold refs to the field and ring, write
ringRef.current.style.opacity in the handler. Use state only for tunables like
radius/exponent, and mirror them into refs so the handler reads current values.
Pattern 2 — Magnetic target
The element leans toward the cursor as it approaches, so the click region
effectively grows in the direction of travel. Same proximity value, applied to a
transform:
const btn = document.querySelector(".magnetic");
const RANGE = 140, MAX = 14; // px sensing radius, px travel
btn.addEventListener("pointermove", (e) => { // listen on a zone around it
const r = btn.getBoundingClientRect();
const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
const pull = Math.max(0, 1 - Math.hypot(e.clientX - cx, e.clientY - cy) / RANGE);
const tx = ((e.clientX - cx) / RANGE) * MAX * pull * 2;
const ty = ((e.clientY - cy) / RANGE) * MAX * pull * 2;
btn.style.transform = `translate(${tx}px, ${ty}px) scale(${1 + pull * 0.06})`;
});
btn.addEventListener("pointerleave", () => { btn.style.transform = ""; });
.magnetic { transition: transform 220ms cubic-bezier(0.34, 1.56, 0.64, 1); }
@media (prefers-reduced-motion: reduce) { .magnetic { transition: none; } }
Keep MAX small (10–15px). Past that the button feels like it's dodging the
cursor, which is the opposite of helpful. The overshoot easing
(cubic-bezier(0.34,1.56,0.64,1)) makes the spring-back feel physical. Guard
on prefers-reduced-motion — bail out of the handler entirely for users who
asked the OS to reduce motion, since this one literally moves things.
Pattern 3 — Trajectory prediction (the useful one)
The strongest anticipation isn't proximity, it's heading: figure out which target the cursor is aimed at and arm it early — highlight it, or kick off the fetch its destination needs, during the ~200ms between intent and click.
Track a smoothed velocity, then for each candidate take the dot product of the
heading with the direction to that candidate. A dot near 1 means "aimed
straight at it." Require a forward cone so a glancing pass doesn't count, and add
a gentle distance penalty so a nearer aligned target wins ties. When the pointer
is nearly still there's no heading to read, so predict nothing — don't silently
fall back to nearest-target, or every paused cursor lights something up.
Update velocity in the pointermove handler, but run the selection in a single
requestAnimationFrame loop reading the latest velocity. That decouples the cost
from event frequency and gives you one place to also draw a heading indicator.
let last = null, vx = 0, vy = 0, pointer = null;
zone.addEventListener("pointermove", (e) => {
if (last) {
const dt = Math.max(1, e.timeStamp - last.t);
vx = vx * 0.7 + ((e.clientX - last.x) / dt) * 0.3; // EMA, px/ms
vy = vy * 0.7 + ((e.clientY - last.y) / dt) * 0.3;
}
last = { x: e.clientX, y: e.clientY, t: e.timeStamp };
pointer = { x: e.clientX, y: e.clientY };
});
let predicted = null, armAt = 0;
function frame(t) {
if (pointer) {
const speed = Math.hypot(vx, vy);
let best = null, bestScore = -Infinity;
if (speed > 0.04) { // moving → has a heading
const nvx = vx / speed, nvy = vy / speed;
for (const el of targets) {
const r = el.getBoundingClientRect();
const dx = r.left + r.width / 2 - pointer.x;
const dy = r.top + r.height / 2 - pointer.y;
const d = Math.hypot(dx, dy) || 1;
const align = (dx / d) * nvx + (dy / d) * nvy; // -1..1
const score = align - d / 1600; // aligned + near wins
if (align > 0.62 && score > bestScore) { bestScore = score; best = el; }
}
}
if (best !== predicted) { // prediction moved
highlight(predicted, false); highlight(best, true);
predicted = best; armAt = t; // restart the hold timer
} else if (best && t - armAt > 160) {
prefetch(best); // held → confidence → prefetch
}
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
The lesson the demo on seangeng.com learned the hard way: arm only the single
current prediction, and clear it the instant the prediction moves. A "prefetched"
flag that accumulates across targets ends with everything lit, which destroys the
signal — the whole point is that one thing reacts. Make a real prefetch cache
idempotent and invisible; keep the visible armed state tied to the live
prediction. Reset everything on pointerleave.
This is the genuinely productive version: submenus that open along the path to them, links that prefetch when the user is clearly headed there, a search index that warms as the pointer drifts toward the box. Prefetch is a hint, not a commitment — debounce it (only fire once a target stays predicted for a breath) so a sweep across the row doesn't fire five fetches, and make sure the real click still works if the prefetch hasn't finished.
Putting it together / checklist
When you implement any of these, verify:
- The control works fully without the effect (keyboard, touch, SR).
- Applied to only 1–2 high-value targets, not sprinkled everywhere.
- Hot path writes to the DOM directly; no per-event framework re-render.
- Decorative overlays are
aria-hidden; listeners reset onpointerleave. -
prefers-reduced-motiondisables anything that moves (patterns 2 and any animated arming in 3). - Prefetch (pattern 3) is debounced and never blocks the real action.
Credit: the proximity-ring formulation is from @gabriell_lab.