Speed
Slow motion multiplier
1×
Reduced motion
Show

Reduced motion

Criterion 10 — BlockerRail: Off
Degrade or delete. Everything else in the sweep is blocked on this answer, because the branch gets written once per file and there are 36 of them.Decide here: Degrade or delete. The decision the 36-file sweep is waiting on.
Use the Reduced motion switch in the rail above
It is the page’s primary control, not a caption: it drives the real policy for every demo in the lab, on this page and every other one. Set it to Delete, then Degrade, and watch the same components under each. Right now it is set to Off.

The sweep’s worklist

40 of 88 animated files have no prefers-reduced-motion branch at all — measured against the installed package. The brief says 39; the gap is counting method rather than a disagreement worth chasing, but the list below is the one to work from because it names the files.
ComponentFilesPaths
Table6core/Table/TableRow.tsx · core/Table/plugins/columnResize/useTableColumnResize.tsx · core/Table/plugins/groupedRows/useTableGroupedRows.tsx · core/Table/plugins/rowExpansion/useTableRowExpansion.tsx · core/Table/plugins/stickyColumns/useTableStickyColumns.tsx · core/Table/plugins/tree/useTableTreeData.tsx
TopNav4core/TopNav/TopNavItem.tsx · core/TopNav/TopNavMegaMenu.tsx · core/TopNav/TopNavMegaMenuItem.tsx · core/TopNav/TopNavMenu.tsx
Chat2core/Chat/ChatComposer.tsx · core/Chat/ChatComposerDrawer.tsx
DropdownMenu2core/DropdownMenu/DropdownMenu.tsx · core/DropdownMenu/DropdownMenuSubMenu.tsx
TabList2core/TabList/Tab.tsx · core/TabList/TabMenu.tsx
(shared)1core/hooks/useKeyboardHint.tsx
Chart1lab/Chart/ChartAxis.tsx
ChatReasoning1lab/ChatReasoning/ChatReasoning.tsx
Citation1core/Citation/Citation.tsx
ClickableCard1core/ClickableCard/ClickableCard.tsx
Collapsible1core/Collapsible/Collapsible.tsx
CommandPalette1core/CommandPalette/CommandPaletteInput.tsx
ComplexSelector1core/ComplexSelector/ComplexSelector.tsx
ContextMenu1core/ContextMenu/ContextMenu.tsx
DateInput1core/DateInput/MonthScroller.tsx
InfoTip1lab/InfoTip/InfoTip.tsx
Item1core/Item/Item.tsx
Link1core/Link/Link.tsx
MultiSelector1core/MultiSelector/MultiSelector.tsx
Outline1core/Outline/Outline.tsx
Pagination1core/Pagination/Pagination.tsx
Resizable1core/Resizable/ResizeHandle.tsx
SegmentedControl1core/SegmentedControl/SegmentedControlItem.tsx
SelectableCard1core/SelectableCard/SelectableCard.tsx
Selector1core/Selector/Selector.tsx
Thumbnail1core/Thumbnail/Thumbnail.tsx
Token1core/Token/Token.tsx
Tour1lab/Tour/TourStep.tsx
TreeList1core/TreeList/TreeListItem.tsx
Table carries 6 of them and TopNav 4, so two components are more than a quarter of the sweep. Both are also the components where a wrong answer is most visible.

Degrade or delete

PolicyWhat survivesForAgainst
DeleteWhat the published page instructs today, and what most of core does.
Nothing. Transitions collapse to zero, loops stop, the end state arrives on the same frame as the change.Safest reading of the accessibility guidance: a user who asked for no motion gets no motion, with no argument about which motion was the harmful kind. Trivially lintable, and it is one line per file.Deletes feedback that happens to be implemented as a transition. A hover tint, a focus ring fade and a spinner all disappear together, and the spinner leaves nothing behind that says "working".
DegradeWhat rubric criterion 10 proposes. A change of published policy, not a bug fix.
Opacity and colour. Movement multipliers go to zero; loops stop and are replaced with a determinate state.Keeps state feedback while removing the vestibular trigger, which is what both of the project’s references recommend. The thing that hurts is movement, not the fact that a colour took 175ms to arrive.Needs a judgement per component about which half is movement, so it is not a one-line sweep. Gets it wrong quietly: a fade that survives on a surface that also travels is still motion.
This is a change of published policy, not a bug fixReversal
When it’s enabled, replace animations with instant state changes.” — Respecting User Preferences, published today
Deleting motion is not an oversight in the components either — it is what the page instructs. The open question in the brief already asks degrade-or-delete; what the brief does not say is that "delete" is the currently published answer, so degrading is a change of policy rather than a fix.Needs a designer and accessibility sign-off, then a rewrite of this paragraph. The 39-file sweep writes whichever branch is chosen 39 times, so it cannot start first.The conflicts, side by side · the published page

The three policies, running

A spinner, a pulse and an indeterminate bar under each answer. Which of these still tells you the system is working?
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 — the published answer
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 — the proposed answer
Degrade: loops stop and are replaced with a static determinate state. Opacity and colour feedback survive; movement does not.
Slowing a loop is the one answer nobody is arguing for and the one the code does today. Between the other two, the question is not whether the spinner keeps spinning — it does not, either way — but whether anything replaces it.

Hover the rows, then flip the rail

The tint is state feedback that happens to be implemented as a transition. Does the policy take the feedback or only the fade?
Hardcoded 150ms
Row one
Row two
Row three
Hardcoded 150ms with the CSS default curve.
Tokenised --duration-state
Row one
Row two
Row three
The state token, at 175ms with --ease-state.
Set the rail to Degrade and hover: movement stops across the lab and these rows keep their fade. Colour is not movement, and this tint is exactly the feedback degrade exists to preserve — which is why delete is defensible on a hover tint and indefensible on a spinner, where the motion is the message.The left pane makes the token argument at the same time: it hardcodes 150ms, so it fades identically under every policy. A literal cannot be reached by a switch, a media query or a sweep.Try it: on Degrade the right pane keeps its tint and loses only movement — the state feedback survives. On Delete the tint snaps, and the affordance goes with it. That is the whole decision, in one hover. (This behaviour was itself a bug in the lab until the policy was moved out of the stylesheet: the store writes the duration aliases inline, and an inline custom property outranks any selector, so the delete rule looked right and did nothing.)

The 0.01s finding

14 of the 59 hardcoded values are 0.01s, and every one is a reduced-motion escape in BottomSheet, DateInput or MobileNav. This is load-bearing, not sloppiness: a zero duration fires no transitionend, so anything sequencing off that event — an unmount, a focus return, a scroll lock release — never runs. The idiom is a component author working around the platform.
SiteComponentProperty
core/BottomSheet/BottomSheet.tsx:94BottomSheettransitionDuration
core/BottomSheet/BottomSheetPanel.tsx:149BottomSheettransitionDuration
core/BottomSheet/BottomSheetSwitcher.tsx:105BottomSheettransitionDuration
core/DateInput/TouchDateField.tsx:256DateInputtransitionDuration
core/DateInput/TouchDateField.tsx:313DateInputtransitionDuration
core/DateInput/TouchDateField.tsx:375DateInputtransitionDuration
core/DateInput/TouchDateField.tsx:392DateInputtransitionDuration
core/DateInput/TouchDateField.tsx:442DateInputtransitionDuration
core/DateInput/TouchDateField.tsx:494DateInputtransitionDuration
lab/Drawer/Drawer.tsx:230DrawertransitionDuration
lab/Drawer/Drawer.tsx:281DrawertransitionDuration
lab/Drawer/Drawer.tsx:380DrawertransitionDuration
core/MobileNav/MobileNav.tsx:132MobileNavtransitionDuration
core/MobileNav/MobileNav.tsx:162MobileNavtransitionDuration
--duration-instant cannot be assumed to be 0Ship it as 0.01ms and every existing sequencing path keeps working, at a cost no user can perceive. Ship it as 0 and these ten sites have to be rewritten to await something else in the same change — which is a different, larger piece of work than the sweep is scoped for.
Decide it before the sweep, not duringThe sweep rewrites these ten lines whichever way the token goes. Deciding halfway through means two idioms in the codebase and a lint rule that cannot tell which one it is looking at.

The rule, as code

What a component author copies. Movement is a multiplier so the policy lands in one place; loops stop rather than slow; hover motion is gated on a fine pointer because touch fires a hover on tap; and the attribute branch exists because an app with its own reduce-motion setting can flip a JS library but cannot flip a media query.
The degrade branchcss
/* Movement is a multiplier, so the policy has one place to land. */
.surface {
--motion-translate: 1;
--motion-scale: 1;
transform: translateY(calc(8px * var(--motion-translate)))
scale(calc(1 - 0.06 * var(--motion-scale)));
transition:
transform var(--duration-enter) var(--ease-entry),
opacity var(--duration-enter) var(--ease-entry),
background-color var(--duration-state) var(--ease-state);
}
/* Degrade: movement goes, opacity and colour stay. Not display: none for
motion — the feedback is the point, the travel is the problem. */
@media (prefers-reduced-motion: reduce) {
.surface {
--motion-translate: 0;
--motion-scale: 0;
}
/* Loops stop. They do not slow down: slower vestibular motion is still
vestibular motion, and a 3s spinner reads as a hung interface. */
.spinner,
.progressIndeterminate {
animation: none;
}
}
/* Hover motion is gated on a fine pointer as well. A touch device fires a
hover on tap, so an un-gated hover animation runs on every tap. */
@media (hover: hover) and (pointer: fine) {
.row:hover {
transform: translateY(calc(-1px * var(--motion-translate)));
}
}
/* An app with its own reduce-motion setting can flip a JS library but cannot
flip a media query, so the same rules answer to an attribute too. */
[data-reduced-motion='reduce'] .surface {
--motion-translate: 0;
--motion-scale: 0;
}
useReducedMotion() — the JS half of the same policytsx
/**
* The JS half of the same policy. Charts, canvas and any motion library read
* this; CSS reads the media query. They have to agree, or a chart animates
* while the interface around it does not.
*/
export function useReducedMotion(): boolean {
const [reduced, setReduced] = useState(() =>
typeof window === 'undefined'
? false
: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
);
useEffect(() => {
const query = window.matchMedia('(prefers-reduced-motion: reduce)');
const onChange = (event: MediaQueryListEvent) => setReduced(event.matches);
query.addEventListener('change', onChange);
return () => query.removeEventListener('change', onChange);
}, []);
// The app-level override, so a product setting can force the policy on
// without touching the OS. Same attribute the CSS above answers to.
const forced = useContext(MotionPreferenceContext);
return forced ?? reduced;
}
Both halves ship together or the system disagrees with itself: a chart that animates while the interface around it has stopped is worse than either policy applied consistently.