Finding Missing Motion
This is a filter that happens to also be a finder. The default posture is rejection: assume the interface needs no more motion than it has, and make every candidate earn its place by surviving a four-question gate in order. An opportunity finder with a low bar is actively harmful — it produces the over-animated, sluggish interfaces the rest of this pack exists to prevent — so reject more than you propose, cap the output at five to seven for a whole app, and treat "this already has the right amount of motion" as a successful result.
The sibling is motion-audit, and the split is whether the motion exists yet: motion-audit sweeps animations that are present and wrong and returns a prioritized fix plan, while this skill only proposes motion that is absent and never returns a verdict or a plan. It also never implements — hand a surviving row to motion-audit plan <candidate> and let that skill specify it. One narrower boundary: delight decides whether a rare moment deserves extra craft at all; this skill locates the seam and names the recipe once that question is settled.
Write every proposal in the vocabulary the project already has. Before hunting, read the repo's easing and duration tokens, its motion library (motion/framer-motion, React Spring, GSAP, WAAPI, or plain CSS), and which curve family it uses — the settling one that lands on 1 or the overshooting one that passes it. A proposal naming a curve the repo does not own, or requiring a library to do what a CSS transition already does, fails its own gate and never reaches the table.
The Gate
Every candidate answers all four, in order. Record the answers — they go in the report.
1. Frequency. The frequency gate run backwards: the tiers that forbid animation also forbid proposing it.
| Frequency | Verdict |
|---|---|
| 100+ times/day — shortcuts, command palette, core navigation | Reject. No animation. Ever. |
| Tens of times/day — hover states, list navigation, frequent toggles | Reject, or propose only near-imperceptible motion |
| Occasional — modals, drawers, toasts, settings | Eligible, standard animation |
| Rare or first-time — onboarding, empty states, success, celebration | Eligible, and this is where the delight budget lives |
Keyboard-initiated actions are a disqualifier, not a judgment call.
2. Purpose. Name it in one of these words or reject: feedback, spatial consistency, state indication, preventing a jarring change, explanation (marketing and onboarding only), or delight (rare tier only). "It looks cool" is not on the list.
3. Speed. The proposal must fit the budget — press 100–160ms, tooltips 125–200ms, dropdowns 150–250ms, modals and drawers 200–500ms, product UI under 300ms. A moment that only works as a slow, showy animation fails.
4. Function. Data the user is reading or acting on must not move for style — decoration helps on a marketing page and hinders on a banking app's graph.
Core Principles
Sweep for seams, not for surfaces. Scanning component by component produces a wishlist, because every component can be made to move; scanning known seam classes produces evidence. The sweep is done when each of the seven classes below has either yielded a candidate with
file:lineevidence or been explicitly cleared. Exception: a single-view request, where the seam list holds but the cap drops to two or three.Reject at the first failed question and stop evaluating. A candidate that fails frequency cannot be rescued by a great purpose, and reasoning on past the failure is how a rejection becomes a proposal. Exception: none — the gate is ordered because the order carries the decision.
Never propose motion on a keyboard-initiated action, at any tier. These fire hundreds of times a day, and animation makes them feel disconnected from the keystroke; Raycast ships no palette open/close animation, which is the right answer rather than an oversight. Exception: none — treat a request to animate a shortcut as a rejection to report, not a candidate to refine.
Carry exact values in every proposal. A row saying "add a subtle entrance" cannot be executed and will be implemented as a 400ms fade; a row saying
@starting-stylefromopacity: 0; translateY(100%)over400ms, exiting by the same edge, can. Use the repo's token names. Exception: a value that genuinely depends on measured travel distance — give the range plus the rule for picking inside it.Propose
transformandopacityonly. They skip layout and paint, so proposing an animatedheightormarginis asking for a jank finding later; expansion uses grid-rows orclip-path: inset()instead. Exception: a rare, one-shot moment on an isolated element with nothing else competing for the main thread — state the tradeoff in the row.Attach the accessibility path to the proposal, not to a follow-up. Motion proposed without a reduced-motion path arrives incomplete and usually ships that way. Every row that moves something carries a gentler variant — keep opacity and color, drop movement — and every hover proposal carries
@media (hover: hover) and (pointer: fine). Exception: opacity-only cross-fades, which are already the reduced-motion path.Publish the rejections. The rejected list is what separates this skill from a wishlist, and it is the part a reader can check your judgment against. List two to five, each with the gate question that killed it. Exception: a request too small to have produced five candidates — list what you actually considered.
Report "nothing survived" plainly when nothing survives. Daily use argues for less motion, not more, and a restrained interface is the goal state. Exception: if the sweep found nothing because the surfaces animate already but animate badly, that is
motion-audit— output a one-line route.
Where to Hunt
| Seam | What it looks like today | Candidate |
|---|---|---|
| Feedback gap | Pressable element with no :active state |
transform: scale(0.97) on :active, transition: transform 160ms ease-out |
| Destructive slip | Delete confirmed by a single plain click | Hold-to-confirm: clip-path: inset(0 100% 0 0) overlay, 2s linear on press, 200ms ease-out snap-back |
| Teleporting state | Conditional render, route content, or section that swaps instantly | Enter from scale(0.95–0.97) + opacity: 0, ease-out, via @starting-style; never scale(0) |
| Missing spatial story | Panel, popover, or menu appearing with no link to its trigger | Scale in from transform-origin: var(--transform-origin); modals stay centered |
| Asymmetric exit | A surface that leaves by a different path than it arrived | Same edge both ways, using percentage translates such as translateY(100%) |
| Flat group entrance | Grid or list popping in all at once on an occasional surface | 30–50ms stagger, 80ms ceiling for long lists, never blocking interaction |
| Physics-free gesture | Draggable or swipeable element that snaps | Spring { type: "spring", duration: 0.5, bounce: 0.2 }, dismissal at velocity > 0.11, rubber-banding instead of a hard stop |
Useful sweeps: {isOpen &&, display: none toggles, onClick with no :active rule, details/accordion markup, drag handlers, .map( renders of entering lists, empty-state and success components.
Smell / Fix
| Smell | Fix |
|---|---|
| A proposal on a command palette or shortcut | Move it to the rejected list with the frequency reason |
| A row whose purpose is "it feels flat" | Name one of the six purposes or drop the row |
| "Add a smooth transition here" | The exact property, duration, curve token, and origin |
| A proposal that needs a new animation library | Do it with a CSS transition, or drop it |
| Animated line-drawing on a chart the user is reading | Reject on question 4 and say so |
| A rejected list that is empty | You were not filtering, you were listing |
| The skill wrote the animation | It reports only; route to motion-audit plan <candidate> |
Output Format
Part 1 — Opportunities, ordered by leverage, one row each:
| # | Location | Today | Purpose | Frequency | Suggested motion |
The Suggested motion cell carries exact properties, durations, and the repo's own curve token, plus the reduced-motion and hover-gating notes where they apply.
Part 2 — Rejected candidates (required). Two to five places you considered and deliberately did not propose, each with the gate question that killed it — CommandMenu.tsx:12, rejected as keyboard-initiated and 100+/day; Chart.tsx:88, rejected as functional data the user is reading.
Part 3 — Verdict. One short paragraph: how much motion this interface actually needs, whether it is already close to right, and which single row has the highest leverage. Close by naming the handoff — motion-audit plan <candidate>.
Checklist
- Read the repo's motion tokens, library, and curve family before hunting
- All seven seam classes either yielded evidence or were explicitly cleared
- Every candidate ran the four gate questions in order, stopping at the first failure
- No proposal touches a keyboard-initiated or 100+/day surface
- Every proposal names one of the six valid purposes and stays under
300mson product UI - Every proposal animates
transformandopacityonly, in existing tokens - Reduced-motion path and hover gating attached to the rows that need them
- Output capped at 5–7 for an app, fewer for one view
- Rejected candidates published with the killing gate question
- Nothing implemented; handoff to
motion-audit plan <candidate>named