Speed1×
Slow motion multiplier
Reduced motion
Show
Rubric bench
12 criteriaHow it grades
No open blockers, and at most two open "should fix" items. Polish items are recorded and never block.| Severity | What it means | Effect on the gate |
|---|---|---|
| Blocker | The motion is wrong in a way that costs comprehension, accessibility or frame rate. | Open blockers fail the review. No exceptions, no "fix it in the follow-up". |
| Should fix | The motion works but is not what the system would do. | Two open items are tolerated. A third fails, so the debt cannot accumulate quietly. |
| Polish | A judgement call about feel, where reasonable people differ. | Recorded, never blocking. Argue it in the review, not in the gate. |
The twelve
1. Justified motion
BlockerHuman judgementPassesThe reason survives the question "what breaks if we delete it?".
Fails"It looks good" on an element the user sees dozens of times a day.
2. Frequency-appropriate
BlockerHuman judgementAlready publishedPassesA checkbox tick, a table row hover and a menu keyboard traversal are instant.
FailsA 200ms draw-on tick, on a control used forty times in a form.
Fails — a tick that draws itself, on a control used forty times in a form
Draws along its path
Passes — instant, because this is a 100+/day interaction
Appears instantly
3. Token fidelity
BlockerMechanical todayPassesEvery value is a token, and the pair is complete.
FailstransitionDuration: '150ms', or a duration with the curve left to the CSS default.
4. Semantic correctness
Should fixPartly — lint narrows it, a human decidesPasses--duration-exit with --ease-exit: the surface leaves and gets out of the way.
Fails--duration-exit with --ease-entry: an exit that spends its travel in the first frames and then hangs.
Fails — exit duration on the entry curve
Passes — exit duration on the exit curve
5. Duration budget
Should fixMechanical todayPassesEnter at 230ms, overlay at 410ms.
FailsA 600ms hover, or an 800ms dialog that has to be sat through before the first click.
--duration-enter
Inside the interface budget
6. Compositor-only
BlockerPartly — lint narrows it, a human decidesPassesA marker that moves with translateY and scaleY, so the list never re-lays-out.
FailsAnimating top and height on a list marker, which runs layout for the whole list every frame.
Fails — animates top and height
Animates top and height, so every frame runs layout, paint and composite for the whole list.
Passes — transform only
Animates transform only, using scaleY on a 1px marker. Compositor-only, so the list never re-lays-out.
7. Enter and exit
BlockerPartly — lint narrows it, a human decidesReverses published guidancePassesA panel that slides in from the right slides back out to the right, faster, on the exit curve.
FailsA surface that animates in and is removed on the next frame.
Fails the criterion — and follows the published page
No exit at all
Passes — exit retraces the entry, faster
Exits on --duration-exit
8. Origin and physicality
Should fixPartly — lint narrows it, a human decidesAlready publishedPassesA menu growing out of the corner of the button that opened it.
Failsscale(0) — nothing in the physical world appears from nothing.
Fails — scale(0)Nothing in the real world appears from nothing. Start from 0.9–0.97 with opacity instead.
scale(0) — appears from nothing
Half right — 0.94, wrong originRight scale value, wrong origin: the surface grows out of its own middle rather than out of the control that opened it.
Scales from its own centre
Passes — anchored to the triggerAnchored to the trigger corner, which is what the published page already asks for.
Scales from the trigger corner
9. Interruptibility
Should fixPartly — lint narrows it, a human decidesPassesInterrupt the travel and the element continues from where it is.
FailsInterrupt it and the element teleports back to the start before running again.
Fails — a keyframe restarts from zero
A keyframe restarts from zero. Interrupt it mid-flight and the box teleports back to the start before running again.
Passes — a transition retargets
A transition retargets from wherever the box currently is, so an interruption is continuous. Springs behave the same way and also carry velocity through.
10. Reduced motion
BlockerMechanical todayReverses published guidancePassesThe loop stops and is replaced by a determinate state; the tint still tells you the row is hovered.
FailsA spinner slowed to 3s, which is still vestibular motion and now also reads as a hang.
Today — loops slow to 3s
Today the loop slows to 3s rather than stopping. Slower vestibular motion is still vestibular motion, and it reads as a hung interface.
Delete — what the published page says
Delete: every loop stops. What the published page instructs — and a spinner that does not spin says nothing at all unless it is replaced with a determinate state.
Degrade — what the criterion says
Degrade: loops stop and are replaced with a static determinate state. Opacity and colour feedback survive; movement does not.
11. Cohesion
PolishHuman judgementPassesA crisp component in a crisp library; a list that enters at 50ms intervals.
FailsOne bouncy component in a library that is otherwise crisp. That is a finding, not a flourish.
12. Library compatibility
Should fixMechanical todayPassesPress feedback on the independent scale property, so a motion library can own transform.
Failstransition: transform on the root — every transform a library writes gets re-eased, so drags lag and springs never settle.
Fails — transition on transform
Press it, then run the drag. The CSS transition re-eases every transform the library writes, so the drag lags the pointer and a spring never settles.
Passes — press on the scale property
Press feedback on the independent scale property, so it no longer collides with the library’s transform writes. Both work — this is the middle path worth prototyping.
What the rubric would have caught
Measured against@astryxdesign/core by the audit script, not read out of the brief. These are the rows a reviewer would have been handed on the day the component landed.Criterion 6 — compositor-only
| Site | Declaration |
|---|---|
| core/Banner/Banner.tsx:340 | transitionProperty: 'transform' |
| core/BottomSheet/BottomSheetPanel.tsx:144 | transitionProperty: 'transform, opacity' |
| core/Button/Button.tsx:91 | transitionProperty: 'background-image, background-color, color, opacity, transform' |
| core/Chat/ChatDictationButton.tsx:70 | transitionProperty: 'transform, background-color' |
| core/Chat/ChatLayoutScrollButton.tsx:71 | transitionProperty: 'opacity, transform, max-width' |
| core/CodeBlock/CodeBlock.tsx:207 | transitionProperty: 'transform' |
transform inside a longer property list rather than on its own, so a grep for transitionProperty: ‘transform’ finds barely half of them. That is worth knowing before anyone scopes criterion 12 from a search — the bugs page has the whole list, and the count has already moved twice as the scanner improved.Criterion 10 — reduced motion
| Component | Files with no branch |
|---|---|
| Table | 6 |
| TopNav | 4 |
| Chat | 2 |
| DropdownMenu | 2 |
| TabList | 2 |
| (shared) | 1 |
Criterion 3 — token fidelity
| Site | Property | Value |
|---|
0.01s reduced-motion escape and 8 are the same 150ms tint in Table. The rubric would have caught every one at review time, which is the argument for the lint rule rather than the sweep.Automatable?
4 of 12 are mechanical today — 3, 5, 10, 12. Five more can be narrowed by lint to a question a reviewer answers in one line. Three are judgement and always will be.| # | Criterion | Automatable | What checks it |
|---|---|---|---|
| 1 | Justified motion | Human judgement | Reviewer. The justification is written in the diff, not inferred from it. |
| 2 | Frequency-appropriate | Human judgement | Reviewer, against how often the surface is actually seen. No tool knows that. |
| 3 | Token fidelity | Mechanical today | Lint. Reads the StyleX rule object; no runtime, no reviewer. |
| 4 | Semantic correctness | Partly — lint narrows it, a human decides | Lint can check the pairing once semantic tokens exist. Whether a surface is entering or leaving is a reviewer call. |
| 5 | Duration budget | Mechanical today | Lint, against the token values. Arithmetic, not taste. |
| 6 | Compositor-only | Partly — lint narrows it, a human decides | Lint reads the animated property list. Whether the surface is large enough for it to matter is a reviewer call. |
| 7 | Enter and exit | Partly — lint narrows it, a human decides | Lint can see whether a component only ever transitions on mount. Whether the exit retraces the entry needs eyes. |
| 8 | Origin and physicality | Partly — lint narrows it, a human decides | Lint catches scale(0) and a missing transform-origin. Whether the origin matches the trigger needs eyes. |
| 9 | Interruptibility | Partly — lint narrows it, a human decides | Lint can flag a keyframe driving a state change. Judging the interrupted feel needs the demo. |
| 10 | Reduced motion | Mechanical today | Lint. An animating file with no prefers-reduced-motion branch is mechanically detectable. |
| 11 | Cohesion | Human judgement | Reviewer, against the rest of the library rather than against a number. |
| 12 | Library compatibility | Mechanical today | Lint plus a render test: forwarded ref, merged className and style, no CSS transition on transform. |
Grandfathering
The rubric is worth nothing if it fails 48 animating components on the day it lands, and worth nothing if it never fails anything. This is the split.| Situation | Rule | Why |
|---|---|---|
| Lab to core promotion | The full rubric applies from day one. No baseline, no allowlist. | Promotion is the one moment where the cost of the rubric is already budgeted, and the same gate already exists for accessibility. |
| A new core component | Full rubric, no baseline. | Nothing to grandfather. |
| Existing core components | Scored once, baselined, and allowlisted with a dated remediation task per blocker. The allowlist can only shrink. | A gate that fails 48 animating components on the day it lands gets turned off in a week. A baseline that can only shrink is the same rule with a schedule. |
| An unrelated change to a failing component | Not blocked. The rule is no new violations against the baseline. | Blocking a copy fix on a motion debt nobody in the diff created is how a gate loses its constituency. |
| A motion change to a failing component | Must clear that component’s blockers before it lands. | If you are already in the motion code, this is the cheapest this fix will ever be — and it is the only way the allowlist actually shrinks. |
A principle the rubric drops
Direction should match the actionPublished, ungraded
“Navigating deeper into content should feel like moving forward. Going back should feel like returning.”
Nothing in the twelve criteria covers directionality.Pagination and Calendar month-change are the surfaces this would be measured against. Both are audit gaps today with no criterion to fail.Recommendation: Fold it into criterion 4 as a fourth pairing rule, or add it as a thirteenth criterion. Either way the rubric should be a superset of the published guidance, not a divergent list.The published page against the proposal, conflict by conflict