Reviewing Motion in a Diff
Default to flagging. Approval is earned, not assumed: motion that runs is not motion that passes, and a transition that merely works while landing from the wrong origin, firing on a keyboard shortcut, or holding the user for 450ms is a regression this diff introduced. Read every motion hunk before judging any line of it, measure each animation against a fixed bar rather than against your mood, and close with one word — Block or Approve. A review that ends in a paragraph of impressions has not been done.
This skill reviews the motion in a diff and nothing else. ui-review owns non-motion UI code; on a mixed diff ui-review runs first and hands its motion hunks here, so never re-report its spacing, type, or contrast findings. Two further boundaries: a whole-codebase sweep that ends in a prioritized fix plan is motion-audit, not this skill, and a rendered screen or screen recording rather than source text is design-critique.
Read the project's own motion vocabulary before writing the first finding. Grep for --ease-, --duration-, the Tailwind theme block, and whichever of motion/framer-motion, React Spring, GSAP, or plain CSS the package manifest actually carries. Review the diff against that system: if the repo defines --ease-drawer, a hand-typed cubic-bezier(0.32, 0.72, 0, 1) in the diff is a finding even though the number is correct, and every fix you propose must be written in the repo's existing tokens. A review that bolts on a parallel easing vocabulary has added the exact debt it was called to prevent.
Quick Reference
| Topic | Where |
|---|---|
| Exact curves, durations, spring configs, velocity and stagger numbers | standards.md — open it the moment a finding needs a number, before you type one from memory |
| Documented exemptions and known false positives | standards.md — open it before reporting anything that looks like a violation but sits on a modal, a marketing page, or a reduced-motion block |
Core Principles
Ask the frequency question before the easing question. A perfectly eased 200ms animation on a command-palette toggle is still a Block, and re-easing it is wasted work: the fix is deletion. The gate is
100+/day → no animation, ever, and any keyboard-initiated action is disqualified regardless of count. Exception: marketing and explanatory surfaces, where a given visitor sees the motion roughly once and the frequency gate does not bind.Flag the five prohibitions on sight and do not deliberate.
transition: all, entering fromscale(0),ease-inon product UI,transform-origin: centeron a trigger-anchored popover, and any animation on a keyboard-initiated action are lint-checkable, so deliberating over them only makes the review slower. Exception: modals legitimately keeptransform-origin: centerbecause they are anchored to the viewport rather than to a trigger — reporting that costs you the reader's trust for every later row.Cite
file:lineand the exact replacement value, never an adjective. "Too slow" cannot be executed, and the author will guess wrong; a reviewable finding readsDrawer.tsx:41 — 500ms ease-in-out → 250ms var(--ease-drawer). Pull the number from standards.md rather than approximating. Exception: when the repo already owns a token for that role, cite the token name and not the literal behind it.Treat any product-UI duration over
300msas a finding unless the diff says why. Perceived responsiveness is dominated by the first 100ms of movement, so a 180ms dropdown reads as faster than a 400ms one that covers the same distance. Exception: modals and drawers may run to500ms, and marketing or explanatory motion is out of budget by design.Prefer deletion to correction, in that order. The remedial hierarchy is delete → reduce → re-ease → fix the origin → make it interruptible → move it to the GPU → make the timing asymmetric → polish → accessibility and cohesion, and an animation that is removed can never regress again. Exception: motion carrying spatial information — where a panel came from, which edge a toast leaves by — where deletion trades one regression for another.
Judge interruptibility by how often the element is retriggered, not by which API it uses.
@keyframesis not itself a defect;@keyframeson a toast stack, a toggle, or a drag is, because keyframes restart from zero while transitions and springs retarget from the current value. Exception: a one-shot entrance or a loader that always runs to completion, where restarting is unreachable.Do not flag a curve for being unfamiliar — flag a diff for carrying two curve families. Two published families exist and they genuinely disagree: one overshoots past
1(cubic-bezier(0.175, 0.885, 0.32, 1.1)), one settles into it (cubic-bezier(0.23, 1, 0.32, 1)). Either is defensible; mixing them inside one product is the finding. standards.md carries both families and their attribution. Exception: a deliberately playful surface inside an otherwise crisp app, when the diff or a design doc states the intent — note it, do not report it.Do not re-litigate a decision the code already documents. A comment or design note explaining why a drawer runs long is evidence the tradeoff was made deliberately, and reopening it burns the review's credibility. Exception: the accessibility floor — a missing
prefers-reduced-motionpath or ungated:hovermotion is never settled by a comment."I cannot tell whether it feels right" is not a finding. Guessing at feel from source text produces noise that buries the rows that are real. Name the exact interaction to watch and hand it to
design-critique; if the diff also needs it, say so in the verdict. Exception: when the source makes feel deterministic — a 2sease-inon a dropdown needs no screen to judge.
Smell / Fix
| Smell | Fix |
|---|---|
transition: all 300ms |
Name the properties: transition: transform 200ms var(--ease-out) |
transform: scale(0) on enter |
transform: scale(0.95); opacity: 0 |
ease-in on a dropdown, sheet, or menu |
ease-out or the repo's strong custom curve |
transform-origin: center on a popover, dropdown, or tooltip |
var(--transform-origin); leave modals alone |
| Animation on a shortcut, palette toggle, or arrow-key move | Delete the animation |
@keyframes on a toast, toggle, or drag |
A transition or spring so it retargets mid-flight |
Animated width, height, margin, top, or left |
transform and opacity only |
Framer Motion x / y / scale shorthands on a busy page |
The full transform string |
| Enter and exit share one duration on a press-and-hold | Slow the deliberate phase, snap the release |
| A fourth hand-typed cubic-bezier in a repo that has tokens | The existing token |
No prefers-reduced-motion path on anything that moves |
Gentler, not zero — keep opacity, drop movement |
| A verdict of "looks good overall" | Block or Approve, one word |
Output Format
Two parts, in this order. The table form is mandatory — Emil Kowalski's review format explicitly forbids the Before: / After: list, because a list hides how many issues there are and cannot be scanned.
Part 1 — Findings table. One markdown table, one row per issue, columns | Before | After | Why |. Before carries the code as written plus file:line; After carries the exact replacement including the token name; Why is one clause on what it costs the user. No prose between rows.
Part 2 — Verdict. Group any remaining commentary by impact, highest first, omitting empty tiers: feel-breaking regressions · motion that should be deleted · performance · interruptibility and timing · origin, physicality, and cohesion · accessibility. Then state the decision on its own line.
- Block — any feel-breaking regression, any animation on a keyboard-initiated or 100+/day action, any of the five prohibitions, or a non-GPU animation with a one-line GPU fix.
- Approve — no feel-breaking regressions, nothing that should be deleted, durations and curves inside budget and inside one family, interruptibility handled where the element is retriggered, and a reduced-motion path present.
An empty findings table with a fast Approve is a real outcome. Manufacturing rows to look thorough costs more than it buys.
Checklist
- Read every motion hunk in the diff before writing a row
- Checked the repo's existing easing and duration tokens and written all fixes in them
- Frequency question answered for each animation before any easing judgment
- The five prohibitions scanned, with modals exempted from the origin rule
- Every row cites
file:lineand an exact value pulled fromstandards.md - Durations checked against the per-element budget and the
300msproduct ceiling - Interruptibility judged by retrigger rate, not by API
- Curve family checked for mixing, not for unfamiliarity
- Reduced-motion and hover gating checked on everything that moves
- Anything unjudgeable from source handed to
design-critiqueby name, not guessed - Output is one
| Before | After | Why |table plus a one-word verdict