From ab1d195663d9e63bb9957ec15ef4203f8099d35a Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Mon, 2 Nov 2026 21:40:36 -0800 Subject: [PATCH] fix(core): no press scale or card color fade under reduced motion Under reduced motion a press never scales a Button, and ClickableCard, SelectableCard and Thumbnail change their hover and selection colors at once on every pointer. The three cards drop their own copy of the press release and compose the shared pressedAlpha alone. Under the touch presentation their press release fades on the theme's exit duration and easing, as other touch press releases do (component:ClickableCard FR5, component:SelectableCard FR4, component:Thumbnail FR4). The press still shows at once. Under reduced motion that fade takes at most 200 ms, opacity only. A mouse press keeps the cards' desktop press styling. --- .changeset/reduced-motion-button-cards.md | 7 + docs/design/reduced-motion.md | 36 +- docs/specs/components/Button/Button.spec.md | 26 +- .../ClickableCard/ClickableCard.spec.md | 33 +- .../SelectableCard/SelectableCard.spec.md | 48 +- .../components/Thumbnail/Thumbnail.spec.md | 31 +- packages/core/src/Button/Button.tsx | 22 +- .../core/src/ClickableCard/ClickableCard.tsx | 45 +- .../src/SelectableCard/SelectableCard.tsx | 45 +- packages/core/src/Thumbnail/Thumbnail.tsx | 22 +- .../ReducedMotionPress.a11y.chromium.spec.ts | 459 ++++++++++++++++++ .../src/motion/reducedMotionValues.test.ts | 78 +++ 12 files changed, 768 insertions(+), 84 deletions(-) create mode 100644 .changeset/reduced-motion-button-cards.md create mode 100644 packages/core/src/__tests__/ReducedMotionPress.a11y.chromium.spec.ts create mode 100644 packages/core/src/motion/reducedMotionValues.test.ts diff --git a/.changeset/reduced-motion-button-cards.md b/.changeset/reduced-motion-button-cards.md new file mode 100644 index 0000000000000..f37dd9ea46698 --- /dev/null +++ b/.changeset/reduced-motion-button-cards.md @@ -0,0 +1,7 @@ +--- +'@astryxdesign/core': patch +--- + +[fix] Under reduced motion, a pressed Button no longer shrinks, and ClickableCard, SelectableCard and Thumbnail change their hover, border and selection colors at once on every pointer, whatever the theme's duration. A finger's press highlight still fades out on release, within 200 ms. + +@vjeux diff --git a/docs/design/reduced-motion.md b/docs/design/reduced-motion.md index 31995ac89528e..449334d4dbbb8 100644 --- a/docs/design/reduced-motion.md +++ b/docs/design/reduced-motion.md @@ -10,7 +10,11 @@ approved_by: cixzhang approved_at: 2026-10-24 owners: [cixzhang] review_triggers: [motion, interaction, accessibility] -verified_by: [scripts/check-knowledge.mjs] +verified_by: + [ + scripts/check-knowledge.mjs, + packages/core/src/__tests__/ReducedMotionPress.a11y.chromium.spec.ts, + ] architecture: [architecture:interaction-modality] components: [] families: [] @@ -32,8 +36,8 @@ deciding_specs: [] A person who asks their device to reduce motion still sees every change Astryx shows, with no travel, scaling, or springing. On a touch screen a change cross-fades, as iOS does with Reduce Motion and Prefer Cross-Fade, except an -in-place value change, which applies at once; with a mouse, -trackpad, or keyboard the change is immediate. +in-place value change and a hover or selection color, which apply at once; +with a mouse, trackpad, or keyboard the change is immediate. ## Design principles @@ -56,7 +60,7 @@ trackpad, or keyboard the change is immediate. | State | Required representation | Allowed variation | | ------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------- | -| reduced motion, coarse pointer | Opacity-only cross-fade of 200 ms or less (DR2); an in-place value change applies at once (DR2) | A shorter cross-fade | +| reduced motion, coarse pointer | Opacity-only cross-fade of 200 ms or less (DR2); in-place values and colors apply at once (DR2) | A shorter cross-fade | | reduced motion, any other pointer | Immediate change (DR3), except a touch drag's release (DR2) | None | | no reduced-motion preference, theme on `'system'` | Outside this record | Component and theme motion | | a theme on `'always'` (DR2a) | As reduced motion, whatever the person's setting | DR1–DR3 | @@ -75,7 +79,13 @@ trackpad, or keyboard the change is immediate. coast Astryx animates itself after a throw behaves like native momentum under Reduce Motion. An in-place value change, such as a progress fill's length or another continuous value, does not cross-fade: under reduced motion it - applies at once on any pointer, with no added element (DEC-3). + applies at once on any pointer, with no added element (DEC-3). A control's + hover or selection color, such as SelectableCard's selection border and + ring, also applies at once on any pointer, as on iOS (DEC-4). So does the + onset of its press color. A color that changes with a part that cross-fades + follows that cross-fade, such as Switch's track with its thumb + (`component:Switch` FR15). A touch press's release keeps its fade of 200 ms + or less. - **DR2a — A theme may reduce motion for everyone.** A theme's `reducedMotion: 'always'` applies DR1–DR3 for every person, whatever their setting; `'system'` follows each person's setting (`spec:AST-108`). No theme @@ -131,6 +141,7 @@ No normative assets are included. | DR1, DR2 | Real-browser computed animations under reduced motion, coarse pointer | Layer entrance and exit (Tooltip, Popover, menu); Dialog; modal BottomSheet; a presentation switch | An animated transform, size, or position; a spring; a property other than opacity; over 200 ms; a change with no cross-fade | | DR2 momentum | Real-browser computed animations under reduced motion after a touch throw | A flick that coasts and settles at an edge; a release that snaps to a detent, dismisses, or pages to a target | A coast replaced by a cross-fade or stopped at release; a snap, dismissal, or page that travels instead of cross-fading | | DR2 values | Real-browser computed animations under reduced motion, coarse and fine pointer | A determinate ProgressBar's fill and another continuous value changing in place | A value that animates, cross-fades, or adds an element under reduced motion fails | +| DR2 colors | Real-browser computed animations under reduced motion, coarse and fine pointer | Button's press onset; ClickableCard's, SelectableCard's and Thumbnail's hover, border and selection colors | A hover or selection color, or a press onset, that animates or cross-fades; a touch release fade over 200 ms | | DR1, DR3 | Real-browser computed animations under reduced motion, fine pointer | The same components | Any animated property, or a frame where the change is partly applied | | DR4 | Component prop and theme review plus the browser checks above | A component with a motion prop or theme motion value set | A prop or theme value that restores travel or a longer animation under reduced motion | @@ -174,6 +185,21 @@ Under reduced motion they take the new value at once on any pointer. Rejected: a cross-fade between the old and new value on a touch screen. +### DEC-4 — A color-only change of state applies at once + +**Reference:** `design:reduced-motion/DEC-4` +**Decider:** `cixzhang`, `2026-11-03`; companion clause per the owner's motion rule (2026-11-04) + +A control's hover and selection colors, and the onset of its press color, +change where they are and move nothing, so reduced motion has nothing to +remove from them. Under reduced motion they take the new color at once on any +pointer, as iOS does and as an in-place value does (DEC-3). A color that +changes with a part that cross-fades, such as Switch's track with its thumb, +follows that cross-fade, so the two never show different states. A touch +press's release keeps its fade of 200 ms or less, which confirms the tap. + +Rejected: a cross-fade of a hover or selection color on a touch screen. + ## Open questions None. diff --git a/docs/specs/components/Button/Button.spec.md b/docs/specs/components/Button/Button.spec.md index c7eb2a83fa65c..e19ef3c6789af 100644 --- a/docs/specs/components/Button/Button.spec.md +++ b/docs/specs/components/Button/Button.spec.md @@ -16,6 +16,7 @@ verified_by: packages/core/src/ButtonGroup/ButtonGroup.test.tsx, packages/core/src/Button/__tests__/Button.a11y.test.tsx, packages/core/src/Button/__tests__/Button.a11y.chromium.spec.ts, + packages/core/src/__tests__/ReducedMotionPress.a11y.chromium.spec.ts, packages/core/src/Layout/__tests__/edgeCompensation.test.tsx, packages/core/src/utils/pressFeedback.test.ts, ] @@ -37,15 +38,15 @@ system_specs: [spec:AST-005, spec:AST-085, spec:AST-086] ## Contract at a glance -| Area | Contract | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Public contract | None changes. This record states the shipped Button surface: `label`, variant, size, elevation, content mode, loading and `clickAction`, `isInterruptible`, disabled, tooltip, width, form attributes, and link mode. | -| Behavior | A named native `