Skip to content

fix(core): no press scale or card color fade under reduced motion - #8326

Draft
vjeux wants to merge 1 commit into
meta:mainfrom
vjeux:uikit/c01-reduced-motion
Draft

vjeux wants to merge 1 commit into
meta:mainfrom
vjeux:uikit/c01-reduced-motion

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

User impact

For people who turn on reduced motion, a pressed Button still shrinks to 98%, which the setting asks it not to do. Button's colors already change at once under reduced motion, but the cards' don't: ClickableCard, SelectableCard and Thumbnail still fade their hover, press and selection colors, border and ring included, on every pointer.

The cards' press release doesn't change. They compose the shared press release, which fades on the theme's exit tokens on every pointer (spec:AST-094 FR5), and their records now state that release.

Problem and solution fit

design:reduced-motion became current. Checking Button and the cards against it found that Button's press scale has no reduced-motion rule, and the cards keep their color, border and shadow transitions on every pointer.

Under reduced motion, Button stays full size. The cards change their colors at once with a mouse or a trackpad. On a touch screen their colors change at once too, and only opacity may still fade, in 200 ms or less: a finger's press highlight keeps its release fade.

Expected behavior and authority

  • design:reduced-motion DR1: a press never scales a button. component:Button FR15 says the same for Button. This pull request removes FR15's gap note and the matching Current-state bullet, and names the browser case in FR15's verification row. Button's loading Spinner is out of scope here.
  • design:reduced-motion DR3: with a fine primary pointer, a change is immediate.
  • design:reduced-motion DR2, the touch screen: only opacity fades, in 200 ms or less, whatever the theme sets (DR4).
  • component:ClickableCard FR5, component:SelectableCard FR4 and component:Thumbnail FR4 each add the spec owner's wording (2026-11-04): "A touch press shows at once. Under the touch presentation, its release fades on the theme's exit motion tokens (--duration-exit, --ease-exit), per spec:AST-094 FR5; a mouse press keeps the card's desktop press styling." Each record's Governing rules add spec:AST-094/FR5, and its Compatibility line cites the clause.

Card 31: (A), decided by the spec owner cixzhang on 2026-11-03. design:reduced-motion says, beside DR2's in-place-value line: "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." DEC-4 records the decision (rejected: a cross-fade of a hover or selection color on a touch screen). The Switch sentence is the companion clause from the owner's motion rule (2026-11-04), and DEC-4's decider line says so. The user-intent line, the coarse-pointer State row and a new "DR2 colors" verification row carry the same exception, and verified_by names the browser spec.

The records are owner-gated, so they need an engineering owner's /approve-spec <head sha>.

Desktop before and after

  • Desktop is unchanged. Hover and a mouse press on ClickableCard, SelectableCard and Thumbnail fade in and out over the fast duration on the standard curve, as on main (compared below).
  • Touch is unchanged. The press shows at once, and the release fades on --duration-exit and --ease-exit, as on main. Its frames match card 26's preview (below).
  • Under reduced motion: a pressed Button stays full size. The cards' colors change at once on every pointer, and on a touch screen only opacity fades, in 200 ms or less.
  • Button's press overlay is unchanged.

Smallest restoration

  • One global "no transitions" rule would also remove the press highlight's short release fade on a touch screen, which confirms a tap.
  • Chosen: arms on Button's and the cards' own transitions. A fine pointer gets no transition. A touch screen fades opacity alone on --ease-exit over min(var(--duration-exit), 200ms), a disabled card's dimming included.
  • The 200 ms bound is DR2's. reducedMotionValues.test.ts keeps each copy equal to reducedMotionFadeConsts.duration, and reads the min(…, 200ms) spelling.
  • The cards compose the shared pressedAlpha alone, as on main. Since fix(core): a press's release fades on the theme's exit motion tokens #9890 it releases on --duration-exit and --ease-exit, with min(--duration-exit, 200ms) under reduced motion, so the cards keep no copy of it.

Tradeoffs: the 200 ms bound is copied into the components, held by the test above. No API change.

Rebase

On main 9d068fc, which includes #9890 (7f8de29). It merges cleanly into main 4e7823a. By the spec owner's decision (2026-11-04), this commit also carries #9919's fold, and #9919 is closed. Against main:

  • The cards' copies of the press release (SHARED_RELEASE*, touchRelease), their sync test sharedPressRelease.test.ts and the cards changeset are gone.
  • scripts/build-css.test.mjs, the three card unit tests and __tests__/pressState.ts equal main's.
  • Button.spec.md FR15 is main's exit-token clause without the old gap sentence about the 0.98 press scale.
  • The ClickableCard and SelectableCard records keep main's nested-press sentence, then the spec owner's clause, then the focus sentence.
  • In ReducedMotionPress.a11y.chromium.spec.ts, the fine-pointer case expects a finger's release on the exit tokens.

Overlaps with open pull requests

Textual conflicts with this head, in its files: #9783 (Button.spec.md), #9930 (the ClickableCard and SelectableCard records) and #9097 (SelectableCard.tsx). #8872, #8874 and #9997 edit the same files and merge cleanly. Whichever merges second rebases.

Writing rubric results

Run over the five changed records: design:reduced-motion, component:Button, component:ClickableCard, component:SelectableCard and component:Thumbnail.

  • R1 narration: none. The scan for proposes, we decided, previously, a port of and now over the added lines finds nothing. The new FR clauses, Compatibility lines and Builder-impact cells are present tense, and the stale "The record describes shipped behavior" lines are gone from the three card records.
  • R2 research: none in the records.
  • R3 evidence: no verification or basis cell names a pull request, issue or commit.
  • R4 rejected alternatives: design:reduced-motion has four, each one Rejected: line inside its decision (DEC-1 to DEC-4). The Button and card records have none; Button's one "rejected" is behavior text ("A rejected destination renders inertly").
  • R5 change references: none in the added lines (no # followed by digits, /pull/ or /issues/).
  • R6 open questions: component:ClickableCard OQ1 is unanswered and keeps its two options. component:Button OQ1 (checkable) is unanswered and names the receipt that settles it. The other three records have none.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

To check by hand: in Storybook, turn on reduced motion (the OS setting, or DevTools' "Emulate prefers-reduced-motion"). Press a Button with the mouse: it doesn't shrink, and its color changes at once. With touch emulation, tap a card: only its overlay's press fades out, within 0.2 s. Selecting a SelectableCard changes its border and ring at once. With reduced motion off and touch emulation, tap a card: its press shows at once and fades out on the theme's exit timing, as on main. With a mouse, a card's hover and press look as on main.

Local gates at 0fcf4c0, on main 9d068fc, after a fresh pnpm install --frozen-lockfile (exit 0). This head differs from 0fcf4c0 only in the three card records, where the clause reads "A touch press shows at once" (the spec owner's correction, 2026-11-04), so its build is identical and the evidence below carries. At this head, check:knowledge, Prettier, CI=true pnpm lint on the changed records and review:pre pass.

  • Red first (local probe, not part of the diff): this head's ReducedMotionPress.a11y.chromium.spec.ts, run on a Storybook built at main 9d068fc, fails 4 of 6 cases: the Button press scale under reduced motion (DR1), immediate changes with a fine pointer (DR3), and the two touch-screen reduced-motion cases (opacity-only fades, and SelectableCard's selection). The two touch-release cases pass on main, where the cards' release is already the shared one.
  • Browser, Chromium, on a Storybook built at this head: ReducedMotionPress.a11y.chromium.spec.ts 6/6; CardPress.a11y.chromium.spec.ts 9/9; Button's accessibility contract 20/20.
  • Unit and node suites: test:ui 20,275/20,275; test:node 16,514 passed, 2 expected failures, none failed.
  • Static: pnpm build, typecheck, CI=true pnpm lint on the changed files, strict ESLint, Prettier, check:knowledge and review:pre pass.

Card 26's preview against this head

Local probe, not part of the diff. Card 26's preview (built from a03df9c) was mirrored from its deployment. 601 of its 608 files are byte-identical to a local build of a03df9c, including every script and stylesheet. The other 7 are the iframe page (the host adds a noindex meta tag), the story index, the project file and three Storybook manager bundles.

The probe served the mirrored files and this head's build side by side, in real Chromium with phone emulation (402×714, touch, coarse primary pointer), and made one real tap on each card through touch input. When the press controller writes the release, the probe pauses the release animation and seeks it to each time below. At each time it reads the surface's --astryx-press-alpha, the overlay's computed paint, and a screenshot of the card.

Card Reduce Motion Release clock, both builds Strength at 0 / 25 / 50 / 100 / 150 ms, both builds Pixels that differ, this head against card 26
ClickableCard off 125 ms 1 / 0.921 / 0.725 / 0.222 / 0 0 / 0 / 0 / 0 / 0
SelectableCard off 125 ms 1 / 0.921 / 0.725 / 0.222 / 0 0 / 0 / 0 / 0 / 0
Thumbnail off 125 ms 1 / 0.921 / 0.725 / 0.222 / 0 0 / 0 / 0 / 0 / 0
ClickableCard on 125 ms 1 / 0.921 / 0.725 / 0.222 / 0 0 / 0 / 0 / 0 / 0
SelectableCard on 125 ms 1 / 0.921 / 0.725 / 0.222 / 0 24 / 0 / 10 / 0 / 10 in this run; see the note
Thumbnail on 125 ms 1 / 0.921 / 0.725 / 0.222 / 0 0 / 0 / 0 / 0 / 0
  • The overlay's computed paint is equal at every time, on every card, with Reduce Motion on and off.
  • SelectableCard with Reduce Motion on: the 24 and 10 pixels sit inside the card, away from the release, and the strength values are equal. In 8 repeated rounds, the same pattern shows between the two builds 2 times and in card 26's build against itself 2 times, so it comes from the story's own run-to-run rendering. Against the local a03df9c build, the same probe read 0 pixels on every row.
  • Positive control: each card's screenshot at 0 ms differs from its screenshot at 150 ms by 4,026–20,665 pixels, so the probe sees the release.
  • Against main 9d068fc, the same probe reads the same clock and strengths, and 0 pixels on all six rows, so the touch release is unchanged against main.

Desktop against main

Local probe, not part of the diff. Desktop hover and mouse press on the three cards, compared with main's own Storybook build at 9d068fc, at 1024×768 with a fine pointer.

  • With animations frozen: at rest, on hover, pressed, on the first frame after the release, and settled after it, each card's screenshot differs from main's by 0 pixels, and the surface's and overlay's computed styles are equal.
  • With animations running, the first frame after the release lands at a different point of the 125 ms fade in each run. Main against itself differed there in 3 of 3 runs (up to 19,495 pixels), and this head against main in 1 of 3 (Thumbnail, 4,011 pixels, the same count main showed against itself).
  • Positive control on main: rest to hover and hover to pressed differ by 4,016–20,223 pixels per card.

@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 5, 2026 12:08am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 5, 2026, 12:30 AM UTC

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 24, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request is waiting on a spec decision. Its branch adds the draft spec:AST-094 (FR1–FR23, DEC-1, DEC-2), which also proposes amending current architecture:theme-tokens, design:user-states DR1 and DR3, family:buttons and spec:AST-025 FR14. An engineering owner needs to approve or reject it, and update or decline those records, by commenting /approve-spec <full head SHA> here; you can record that yourself if you are an engineering owner. Whatever that decision, these points need changes:

  • Restack once the predecessors land, so this pull request carries only the reduced-motion commit instead of 41 commits and about 29,000 authored lines.
  • Button and the three cards now import ../motion/reducedMotion.stylex, which core's exports map does not list, so astryx swizzle produces an unresolvable import and CI's test-node (4/4) fails (swizzle.test.mjs:153).
  • On a coarse pointer, design:reduced-motion DR2 asks for an opacity-only fade, but Button and the cards still transition colours, borders and shadow (Button.tsx:113–114, SelectableCard.tsx:80), and the DR2 test checks only durations.
  • AST-094 FR8 (a 0.5 s navigation fade, sheets and menus on springs, a throw that keeps moving) contradicts design:reduced-motion DR1 and DR2 and should be conformed to them.

[Automated review]

Comment thread packages/core/src/Button/Button.tsx Outdated
import type {BaseProps} from '../BaseProps';
import type {Elevation, SizeValue} from '../utils/types';
import * as stylex from '@stylexjs/stylex';
import {reducedMotionFadeConsts} from '../motion/reducedMotion.stylex';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

astryx swizzle rewrites this import to @astryxdesign/core/motion/reducedMotion.stylex, but core's exports map does not list that subpath, so a swizzled Button (and ClickableCard, SelectableCard, Thumbnail) gets an import that does not resolve. CI's test-node (4/4) fails on it at packages/cli/api/swizzle/swizzle.test.mjs:153. Moving the constants into an exported .stylex module (for example utils/interactionOverlay.stylex.ts, which already holds pressConsts) would fix it.

Comment thread packages/core/src/Button/Button.tsx Outdated
default: 'background-image, background-color, color, opacity, transform',
// Under reduced motion nothing scales, so nothing transitions transform.
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)':
'background-image, background-color, color, opacity',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under (prefers-reduced-motion: reduce) and (pointer: coarse) this keeps background-image, background-color and color transitioning for 200 ms. design:reduced-motion DR2 requires these transitions to "change opacity only", and its verification row fails "a property other than opacity". The policy's own example in motion/reducedMotion.ts (lines 24–28) uses 'opacity' alone for this key.

@@ -77,8 +78,22 @@ const styles = stylex.create({
':is(:disabled,[aria-disabled="true"])': 'default',
},
transitionProperty: 'box-shadow, border-color',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new coarse-pointer duration at lines 89–90, box-shadow and border-color now animate for 200 ms under reduced motion, but DR2 allows only opacity. Carrying the change on opacity (for example, fading the existing ::after by opacity) would conform.

on: a transition that travels becomes a cross-fade carrying the same change
of state. An entrance lands in place and fades in, an exit fades out in
place, and any other move fades through. A navigation push or pop
cross-fades over 0.5 s on the ease-in-out curve; a sheet, a full-screen

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FR8 asks for a 0.5 s navigation cross-fade, sheets that fade on their presentation spring, menus on a Reduce Motion spring, and a throw that keeps its motion. Touch presentations key on a coarse pointer (FR13), where the current design:reduced-motion record requires an opacity-only change of 200 ms or less, including for a transition the release of a touch drag starts (DR2), and forbids springs (DR1). Please conform FR8 to DR1 and DR2.

try {
const {element} = await transitionsOf(button.page, 0);
expect(element).toBeGreaterThan(0);
expect(element).toBeLessThanOrEqual(200);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This proves the length of the DR2 fade but not that it is opacity-only. Also reading transitionProperty on the element and on its ::after, and expecting only opacity, would catch the colour transitions at Button.tsx:113–114.

Comment thread .changeset/touch-motion-theme-roles.md Outdated
'@astryxdesign/cli': patch
---

[feat] Themes can retune touch motion. `defineTheme({motion})` takes `springs`, `durations` and `curves` keyed by motion role, the table path of a value touch presentations play (`springs.sheetPresent`, `timing.reducedMotionCrossFadeMs`), at the root or in an adaptation rule. A role the theme leaves out keeps its measured default, and the duration scale (`fast`, `medium`, `ratio`) is optional when a theme sets only roles. Adaptation rules gain a `platform` condition, `ios` or `other`. `astryx docs motion` lists every motion role with its CSS variable and default, and the theme authoring guide and template describe the new fields.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timing.reducedMotionCrossFadeMs is excluded from the roles (motion/uikit/timing.ts:158), and .changeset/reduced-motion-record.md says it is no longer a theme role. Naming it here tells theme authors they can lengthen the Reduce Motion fade, which design:reduced-motion DR4 forbids.

Comment thread packages/core/src/theme/defineTheme.ts Outdated
@@ -247,9 +255,15 @@ export interface DefineThemeInput {
* // Snappy: { fast: 100, medium: 250, ratio: 0.75 }
* // Default: { fast: 175, medium: 410, slow: 975, ratio: 0.75 }
* // Cinematic: { fast: 200, medium: 500, slow: 1200, ratio: 0.7 }
*
* // A softer sheet and a slower reduced-motion fade:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example sets navigation.transitionMs, but its caption promises "a slower reduced-motion fade", which DR4 rules out and the code no longer allows.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current CI run concluded skipped. Current analysis is unavailable.


Generated by PR Enrichment workflow | View current CI run

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request is waiting on a spec decision: its branch adds the draft spec:AST-094 (FR1–FR23, DEC-1, DEC-2), which proposes amendments to current architecture:theme-tokens, architecture:interaction-modality, design:user-states DR1 and DR3, family:buttons and spec:AST-025 FR14, and a new stable @astryxdesign/core/motion/reducedMotion.stylex export that its own code marks as pending owner review. An engineering owner needs to approve or reject them by commenting /approve-spec <full head SHA> on this pull request; you can record that yourself if you are an engineering owner. Whatever that decision, these points need changes:

  • Restack once the predecessors land, so this pull request carries only the reduced-motion commit instead of 69 commits and about 30,600 authored lines.
  • On a coarse pointer, design:reduced-motion DR2 asks for an opacity-only fade, but Button and the cards still transition colours, borders and shadow (Button.tsx:113–114, SelectableCard.tsx:80), and the DR2 test checks only durations.
  • AST-094 FR8 (a 0.5 s navigation fade, sheets and menus on springs, a throw that keeps moving) contradicts DR1 and DR2, as do the springs.menuReducedMotion theme role and the animator changeset's spring fade.
  • The theme template documents a platform adaptation axis that defineTheme now rejects (packages/cli/assets/theme.template.ts:361–362).

[Automated review]

Comment thread packages/cli/assets/theme.template.ts Outdated
* `from` includes its point; `below` excludes it. The other supported axes
* are primary-pointer precision, contrast preference, and motion preference.
* are primary-pointer precision, contrast preference, motion preference, and
* platform (`ios` for the browsers of iPhone and iPad, `other` for the rest).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence documents a platform adaptation axis, but the stack removed it: CONDITION_AXES in packages/core/src/theme/themeAdaptations.ts ends at motion, and assertAllowedKeys throws ….when.platform is not supported. A theme author who follows the template gets an error from defineTheme. Restoring the base wording ("primary-pointer precision, contrast preference, and motion preference") would fix it.

* iPhone 17 Pro, `_animateUsingSpringWithDampingRatio:response:tracking:`
* hooked with Reduce Motion on (1.0, 0.4), Oct 23 2026
*/
menuReducedMotion: {response: 0.4, dampingRatio: 1},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

springs.menuReducedMotion is registered as a theme motion role (it appears in astryx docs motion as --motion-springs-menu-reduced-motion). design:reduced-motion DR4 says a theme value offers no way to keep springs under reduced motion, and DR1 forbids springing; excluding it from the roles, as timing.ts does for reducedMotionCrossFadeMs, would keep the theme surface within DR4.

'@astryxdesign/core': patch
---

[chore] The touch presentations' animator plays any closed-form curve (such as a spring chasing a moving target), drives a pseudo-element such as a dialog's ::backdrop, fades on a presentation's own spring under Reduce Motion (with an entrance or exit role for a slide that changes no opacity), and starts a call from rest when the browser commits it, as Core Animation does. Not exported from the package root; touch presentations reach it through the experimental subpaths (see their entry).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This release note says the animator "fades on a presentation's own spring under Reduce Motion", while .changeset/reduced-motion-record.md says it cross-fades in 200 ms and removes call options that no release ever shipped. Both land in the same release; describing only the final behaviour would keep the notes consistent with design:reduced-motion DR1 and DR2.

Comment thread scripts/sync-exports.js Outdated
@@ -177,6 +190,13 @@ const STATIC_EXPORTS = {
types: './dist/utils/interactionOverlay.stylex.d.ts',
default: './dist/utils/interactionOverlay.stylex.js',
},
// Proposed; pending owner review (spec:AST-094 OQ4): the Reduce Motion

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new stable export is justified by "spec:AST-094 OQ4", but the record has only OQ1 and OQ2. The subpath sits outside any experimental boundary, so once released it falls under the stable compatibility promise; it needs an owner decision, and the citation should point at what actually covers it.

follows.
- **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the
exit its UIKit counterpart plays. **Proposed; pending owner review:** this
departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-090 does not exist on main or on this branch, so FR7's "departs from the draft spec:AST-090 FR1 … and FR10" (and the same citation under Current-state impact) points at nothing an owner can read.

Comment thread packages/build/src/vite.ts Outdated
const read = new Set(
[
...css.matchAll(
new RegExp(`var\\(\\s*(--(?:${names})[a-z0-9]+)\\s*[,)]`, 'g'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matching --(?:astryx|x)[a-z0-9]+ also catches a product's own variables such as --xl or --xsmall read from a StyleX style and declared in global CSS; the build then warns that the declaration "would be dropped in every browser", or fails with undefinedVariables: 'error'. Deriving the generated names from the StyleX rules the build collected would avoid the false report.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request is waiting on an engineering-owner decision: its stacked commits add public theme fields (motion.springs, motion.durations, motion.curves), an undefinedVariables option for astryxStylex, and an internal touch animator, gesture system and interaction harness, none of which a current spec covers. Approval needs an engineering owner to identify or provide the governing spec for these additions, or to approve them as proposed, by commenting /approve-spec <full head SHA> here; you can record that yourself as an engineering owner. Whatever that decision, these points need changes:

  • Restack once the predecessors land, so this pull request carries only the reduced-motion commit instead of about 29,700 authored lines.
  • On a coarse pointer, design:reduced-motion DR2 asks for an opacity-only fade, but Button and the cards still animate colour, border and shadow (Button.tsx:112–113, SelectableCard.tsx:79), and the contract checks only durations.
  • The velocity tracker offers four selectable filters and keeps a non-zero release after a rest (VelocityTracker.ts:43, VelocityTracker.test.ts:99), which the current spec:AST-100 FR2 and architecture:interaction-modality INV10 rule out.
  • springs.menuReducedMotion should not be a theme role under DR4, and the new build check also reports an app's own variables such as --xl as undeclared (packages/build/src/vite.ts:411–432).

[Automated review]

* header describes, `uikit` by default.
* @experimental
*/
export type VelocityStrategy = 'uikit' | 'lsq2' | 'iosFling' | 'window';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-100 FR2 and architecture:interaction-modality INV10, current on main, allow one release filter (iOS's blend) and require a release velocity of 0 after the contact stays still longer than the stillness interval. This union adds three alternative filters, PanGestureRecognizer's velocityStrategy option exposes them, and the default "never decays" (line 19). Keeping only the blend, adding the stillness reset and dropping the option would conform.

},
);

it('uikit reports the last moving value after a rest, as UIKit does', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pins the release spec:AST-100 FR2 rules out: its verification row fails "a non-zero release after a still pause". Once the tracker resets after the stillness interval, this case should expect 0 for a lift after a pause longer than that interval.

}, [ref, recognizer, manageStyle]);

useIsomorphicLayoutEffect(() => {
recognizer.setEnabled(isEnabled);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When isEnabled turns false mid-gesture, setEnabled cancels the recognizer (GestureRecognizer.ts:557–573); cancelled is an action state, so the listener above calls the caller's onGesture from inside this layout effect. architecture:react-component-runtime INV2 keeps consumer callbacks out of effects, and INV3 asks a new effect to name the external system it synchronizes.

Comment thread .changeset/reduced-motion-record.md Outdated
'@astryxdesign/core': patch
---

[fix] The experimental touch animator follows design:reduced-motion. Under Reduce Motion every `animate` and `play` (travel or opacity, on a spring or any curve) becomes an opacity-only cross-fade of 200 ms on a coarse primary pointer, or for a call marked `touchRelease`, and completes at once on any other pointer; nothing moves, scales or springs, and travel lands with a plain style write while the element is transparent, never as an animated transform. `timing.reducedMotionCrossFadeMs` is now 200 ms and is no longer a theme role. The `reducedMotion` and `crossfade` call options are removed; `crossfadeAs` stays. `animator.reducedMotion()` reports the mode for feedback with no fade of its own, `reducedMotionFade()` gives the same fade to code beside the animator, and `reducedMotion.stylex.ts` holds its constants for StyleX transitions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The animator is internal with no experimental subpath, and no release shipped the reducedMotion and crossfade options this note says are removed. Describing it as internal, as the sibling gesture-* and uikit-* changesets do, would keep the release notes accurate.

Comment thread packages/lab/tsconfig.json Outdated
"rootDir": "./src",
"paths": {
"@astryxdesign/core/motion/*": ["../core/dist/motion/*"],
"@astryxdesign/core/gestures": ["../core/dist/gestures/index"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No Lab file imports these paths at this head, architecture:interaction-modality INV10 says Lab cannot import the velocity tracker that @astryxdesign/core/gestures exposes, and core's exports map lists none of these subpaths, so a Lab import would not resolve for npm consumers. Dropping this block and its twin in tsconfig.build.json until Lab needs an exported path would avoid that.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
meta#8076 folds main, which clears the conflicts with main. ci.yml is
meta#8076's: the reduced-motion press spec's workflow step becomes a
@Pr-a11y routing line in the spec's header, so a PR that changes Button,
the cards, Thumbnail, the reduced-motion keys or the interaction overlay
runs it.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: under reduced motion on a touch screen, Button, ClickableCard, SelectableCard and Thumbnail still animate colour, border colour and shadow for 200 ms, while design:reduced-motion DR2 allows only an opacity cross-fade there. Please carry those state changes on opacity (for example by fading the ::after overlay) and have the DR2 browser test assert transition-property as well as duration. The diff against main also carries about 19,800 lines from the stacked predecessors listed in the description (#8045, #8056, #8061, #8067, #8071–#8074, #8258, #8076) and the build fix merged into them (#8075), so please land those first so this pull request shows only its own change. The press-scale and fine-pointer fixes behave as intended in Storybook.

[Automated review]

// DR4).
'@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))':
'0s',
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)': '200ms',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under (prefers-reduced-motion: reduce) and (pointer: coarse) the border keeps transition-property: border-color (L181) for 200 ms, and the ::after overlay's background-color (L118–119) gets the same 200 ms at L134–135. design:reduced-motion DR2 asks for an opacity-only change on a coarse pointer, and its verification row fails “a property other than opacity”. Carrying the border and overlay change on a layer whose opacity fades under this arm would conform.

// (motion/reducedMotion.stylex.ts), spelled out because that module is
// internal and a swizzled component imports only exported paths;
// motion/reducedMotionValues.test.ts holds them equal.
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)':

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in ClickableCard, this arm gives the overlay's background-color transition (L221–222) 200 ms on a coarse pointer, where design:reduced-motion DR2 allows only opacity. Fading the overlay by opacity under this arm would conform.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: with reduced motion on a touch screen, Button, ClickableCard, SelectableCard and Thumbnail still animate colour and border colour for 200 ms (selecting a SelectableCard in Storybook runs four 200 ms border-colour transitions), but design:reduced-motion DR2 allows only an opacity cross-fade there, and Button's own record (FR15) keeps its transitions instant under reduced motion. Please carry the cards' touch changes on opacity (for example by fading the ::after overlay or a ring layer), keep Button at 0s under reduced motion on every pointer, and make the DR2 browser test assert transition-property as well as duration. The diff against main also carries about 16,000 lines of stacked work from the predecessors in the description, so please land those first so this pull request shows only its own change.

  • In the stack, the gesture host's fine-pointer action-group exception cites a family:action-groups record that does not exist on main, while AST-094 FR19 says no recognizer attaches under a fine pointer.
  • The gestures README says usePressTracker rebuilds its tracker when its options change, but the hook builds it once from the first render's options.

[Automated review]

Comment thread packages/core/src/Button/Button.tsx Outdated
Comment on lines +117 to +118
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)':
'background-image, background-color, color, opacity',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under reduced motion on a touch screen this arm transitions background-image, background-color, color, opacity for 200 ms. Button's record (FR15) keeps its hover and press transitions instant under reduced motion, and design:reduced-motion DR2 allows only opacity; keeping none / 0s here on every pointer, with the scale(1) arms, conforms to both.

// (motion/reducedMotion.stylex.ts), spelled out because that module is
// internal and a swizzled component imports only exported paths;
// motion/reducedMotionValues.test.ts holds them equal.
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)': '200ms',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a touch screen with reduced motion, selecting a card now runs four 200 ms border-*-color transitions, because the transition-property above still lists box-shadow, border-color. DR2 allows only an opacity cross-fade here: carry the selection change on a layer whose opacity fades, and list only opacity in the coarse arm.

// DR4).
'@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))':
'0s',
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)': '200ms',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This keeps border-color (and the overlay's background-color) animating for 200 ms on a touch screen under reduced motion; DR2 allows only opacity there, so the coarse arm should fade an opacity layer instead.

Comment on lines +237 to +238
'@media (prefers-reduced-motion: reduce) and (pointer: coarse)':
'200ms',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a touch screen under reduced motion the overlay's background-color now fades for 200 ms; DR2 allows only opacity, so fade the overlay's opacity instead of its colour.

Comment on lines +200 to +221
test('on a touch screen, a button’s and a card’s colour changes stay cross-fades of 200 ms or less under reduced motion (DR2)', async ({
browser,
}) => {
const button = await open(browser, PHONE, BUTTON);
try {
const {element} = await transitionsOf(button.page, 0);
expect(element).toBeGreaterThan(0);
expect(element).toBeLessThanOrEqual(200);
} finally {
await button.close();
}
for (const [name, story, index] of CARDS) {
const card = await open(browser, PHONE, story);
try {
const {overlay} = await transitionsOf(card.page, index);
expect(overlay, name).toBeGreaterThan(0);
expect(overlay, name).toBeLessThanOrEqual(200);
} finally {
await card.close();
}
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This DR2 case checks only that the longest duration is above 0 and at most 200 ms, so it passes while Button and the cards transition colour, border colour and shadow. Also read transitionProperty on the element and its ::after and expect opacity alone, and select a SelectableCard to assert which properties actually transition.

* the touch.
* - Where the primary pointer is fine the host listens for nothing
* (`spec:AST-094` FR19, FR24) except an action group
* (`AttachOptions.actionGroup`, `family:action-groups` FR5): one passive

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

family:action-groups FR5 does not exist on main, and AST-094 FR19 says that where the primary pointer is fine no recognizer attaches and no touch listener is registered. This exception needs a current record that grants it, or should be dropped.

Comment thread packages/core/src/gestures/README.md Outdated
Comment on lines +52 to +54
- `usePressTracker` rebuilds its tracker when `trackingExpansion`,
`highlightDelay`, `cancelTravel` or `quickTapFlash` change, from the next
touch, and reads its callbacks (`isBraking` included) on every touch.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hook builds its PressTracker once, in useState, from the first render's options (useGestureRecognizer.ts L136–172), so a later change to trackingExpansion, highlightDelay, cancelTravel or quickTapFlash is ignored. Either rebuild the tracker when they change, or say here that they are read once.

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

(Posted by Cindy's agent.) Sequencing note: folding the three touchRelease copies into one pressedAlpha variant is deferred until #9890 merges, to avoid cross-PR edits mid-flight; follow-up: #9919.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: CI has not run on this head yet. It starts once the pull request leaves draft and a maintainer adds the ci:run label.

Under reduced motion, Button no longer scales on a press (component:Button FR15, design:reduced-motion DR1), the cards' colour changes are immediate with a mouse or trackpad (DR3), and on a touch screen only opacity fades within 200 ms, under the DR2 colour exception and DEC-4 approved for this commit; the cards' exit-token touch release follows the release clauses approved with it.

Non-blocking follow-ups:

  • The three restated touch-release copies in ClickableCard, SelectableCard and Thumbnail fold into one shared pressedAlpha variant in #9919, as deferred on this pull request.
  • Button.spec.md still says the 0.98 press scale applies under reduced motion (FR15) and that this is listed under Current-state impact; both lines can go now.

[Automated review]

@vjeux
vjeux force-pushed the uikit/c01-reduced-motion branch from a32fa92 to a3480a5 Compare November 4, 2026 10:32
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec a3480a5

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: CI has not run on this head yet. It starts once the pull request leaves draft and a maintainer adds the ci:run label.

Under reduced motion, Button no longer scales on a mouse or touch press (component:Button FR15, design:reduced-motion DR1), and ClickableCard, SelectableCard and Thumbnail change their colours at once (DR3, plus the DR2 colour exception and DEC-4 an engineering owner approved for this head). The cards' touch release on the exit tokens follows the approved ClickableCard FR5, SelectableCard FR4 and Thumbnail FR4 clauses.

Non-blocking, for the next rebase:

  • Regression: on main the press controller now waits for a release animation that is still running, so the cards' comment "The controller ends the press on the shared clock, so the clock caps the fade" no longer holds there; drop the cap or restate why it stays.
  • Button.spec.md L47 still lists the press scale under reduced motion as a conflict with DR1, which this pull request fixes.

[Automated review]

Comment on lines +98 to +99
// The controller ends the press on the shared clock, so the clock
// caps the fade.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: on main, endAfterRelease in utils/pressFeedback.ts waits for a release animation that is still running when the clock ends, so after a rebase the controller no longer cuts a longer fade short. Drop the cap, or say why it stays. The same comment is in SelectableCard.tsx and Thumbnail.tsx.

@vjeux
vjeux force-pushed the uikit/c01-reduced-motion branch from a3480a5 to a03df9c Compare November 4, 2026 14:28
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request needs an engineering owner's decision before it can be approved.

Under reduced motion, Button no longer scales on a press (component:Button FR15, design:reduced-motion DR1), and the three cards' colour changes become immediate with a mouse (DR3). The rest rests on record changes this pull request proposes: the design:reduced-motion DR2 colour exception and DEC-4, and the touch-release clause in component:ClickableCard FR5, component:SelectableCard FR4 and component:Thumbnail FR4. The approval given for the previous head does not apply after the rebase, so an engineering owner needs to record the decision by commenting /approve-spec <full head SHA> on this pull request, or reject any of those changes; the author may do so as an engineering owner.

Non-blocking:

  • The comment "The controller ends the press on the shared clock, so the clock caps the fade" in the three cards no longer holds: after the rebase the press controller waits for a running release animation, so keep only the reason for the cap or drop it.
  • Button.spec.md still lists the press scale under reduced motion as a conflict with DR1 in its readiness row.

[Automated review]

Comment on lines +99 to +100
// The controller ends the press on the shared clock, so the clock
// caps the fade.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: after the rebase, endAfterRelease in utils/pressFeedback.ts waits for a release animation that is still running when the clock fires, so the controller no longer ends the press on the shared clock. Keep only the reason, e.g. "Cap the release at the shared press clock until that clock follows the exit tokens." The same comment is in SelectableCard.tsx and Thumbnail.tsx.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d03a931: the three cards now keep only the reason, "Cap the release at the shared press clock until that clock follows the exit tokens."

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec a03df9c

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@vjeux
vjeux force-pushed the uikit/c01-reduced-motion branch from a03df9c to d03a931 Compare November 4, 2026 15:06
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec d03a931

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: CI has not run on this head yet. It starts once the pull request leaves draft and a maintainer adds the ci:run label.

Under reduced motion, Button no longer scales on a press (component:Button FR15, design:reduced-motion DR1), and ClickableCard, SelectableCard and Thumbnail change their colours at once (DR3, and the DR2 colour exception and DEC-4 approved for this head). On a phone, the three cards' tap release follows the theme's exit duration and curve, as their approved record clauses state.

Non-blocking:

  • The description quotes DR2 as "keeps its fade, on the exit tokens within 200 ms"; the record says "keeps its fade of 200 ms or less".
  • The three restated release copies are due to fold into the shared press module in #9919 once #9890 lands, as planned.

[Automated review]

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec c377f91

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested. An engineering owner approved this head's record changes, and the reduced-motion fixes behave as described in Chromium: a pressed Button stays full size, and the cards' colours change at once with only opacity fading on a touch screen. Three regressions from #9890 landing on main remain:

  • Regression: since #9890, every press release fades on --duration-exit and --ease-exit on every device and pointer (spec:AST-094 FR5), so the cards' touchRelease styles (ClickableCard.tsx L79–113, SelectableCard.tsx L77–111, Thumbnail.tsx L145–179), their record clauses and the browser spec's 200 ms fine-pointer case now turn a finger's release on a touchscreen laptop back to 200 ms on the old curve and cap a longer theme exit duration; rebase and delete them, since pressedAlpha already gives the cards that release.
  • Regression: .changeset/cards-touch-release-exit-tokens.md announces that the cards' release now fades on the exit tokens, which main already ships, so delete it with the code above.
  • Regression: the branch no longer merges, with conflicts in Button.spec.md (keep main's FR15 release sentence and remove only the press-scale gap note) and scripts/build-css.test.mjs (take main's release-clock test).

The fix commit needs a fresh /approve-spec <full head SHA> from an engineering owner, since a new commit ends this head's approval.

[Automated review]

* curve. SYNC: utils/interactionOverlay.stylex.ts PRESS_RELEASE_DURATION,
* PRESS_RELEASE_DURATION_REDUCED and RELEASE_EASE.
*/
const SHARED_RELEASE = `calc(${pressConsts.releaseMs}ms * min(var(--_astryx-motion-tempo, 1), 1.5))`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regression: since #9890 landed on main, the shared release in utils/interactionOverlay.stylex.ts is --duration-exit on --ease-exit for every pointer (spec:AST-094 FR5). This restated 200ms × tempo on cubic-bezier(0, 0, 0.2, 1), composed after pressedAlpha, turns a finger's or pen's release on a touchscreen laptop back to 200 ms on the old curve, and the coarse arm caps a longer theme exit duration. Delete touchRelease and these constants; pressedAlpha already gives the card the exit-token release.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0fcf4c0: touchRelease and the SHARED_RELEASE* constants are gone, and the card composes pressedAlpha alone.

* curve. SYNC: utils/interactionOverlay.stylex.ts PRESS_RELEASE_DURATION,
* PRESS_RELEASE_DURATION_REDUCED and RELEASE_EASE.
*/
const SHARED_RELEASE = `calc(${pressConsts.releaseMs}ms * min(var(--_astryx-motion-tempo, 1), 1.5))`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regression: same as in ClickableCard: on main this restated release overrides the exit-token release spec:AST-094 FR5 requires on every pointer. Delete touchRelease and these constants.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0fcf4c0: touchRelease and the SHARED_RELEASE* constants are gone, and the card composes pressedAlpha alone.

* curve. SYNC: utils/interactionOverlay.stylex.ts PRESS_RELEASE_DURATION,
* PRESS_RELEASE_DURATION_REDUCED and RELEASE_EASE.
*/
const SHARED_RELEASE = `calc(${pressConsts.releaseMs}ms * min(var(--_astryx-motion-tempo, 1), 1.5))`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regression: same as in ClickableCard: on main this restated release overrides the exit-token release spec:AST-094 FR5 requires on every pointer. Delete touchRelease and these constants.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0fcf4c0: touchRelease and the SHARED_RELEASE* constants are gone, and Thumbnail composes pressedAlpha alone.

'@astryxdesign/core': patch
---

[feat] Under a coarse primary pointer, the press release of ClickableCard, SelectableCard and Thumbnail now fades on the theme's exit duration and easing, capped at the press's release time.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regression: main already fades these cards' release on the exit tokens, on every pointer (#9890's changeset names cards). This entry would announce a change that is not this pull request's; delete it with the card release styles.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0fcf4c0: the changeset is deleted.

} finally {
await card.page.mouse.up();
}
// A finger's release on this desktop keeps the shared clock and curve.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regression: this pins a finger's release on a desktop at 200 ms on cubic-bezier(0, 0, 0.2, 1); on main, spec:AST-094 FR5 makes it --duration-exit on --ease-exit. Once the card release styles go, expect the exit tokens here (as the touch-screen case above does).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0fcf4c0: the fine-pointer case expects a finger's release on the exit tokens.

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 0fcf4c0

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: CI has not run on this head yet. It runs once the pull request leaves draft and a maintainer adds the ci:run label, since the branch is in a fork.

Under reduced motion, a pressed Button now stays full size and the three cards change their colours at once, with only opacity fading on a touch screen within 200 ms; an engineering owner approved the record changes for this head, and the earlier findings are fixed.

  • Optional wording fix: "A press shows at once" in the ClickableCard, SelectableCard and Thumbnail clauses also reads as covering a mouse press, which still fades in over --duration-fast; "A touch press shows at once" would match the code.

[Automated review]

| FR2 | A primary-button press on the card surface calls `onClick` once with the card as `currentTarget`, then performs an accepted navigation unless the callback cancels it. A middle press on the surface of a card given `href` opens an accepted destination in a new tab without calling `onClick`. A press inside a nested interactive descendant, inside an open Tooltip or HoverCard surface (`component:Tooltip` FR11), or one that ends a text selection, does not activate the card. Activating the hidden button of an action card calls `onClick` once; activating the hidden link navigates without calling `onClick`. | Current source and tests; `architecture:public-component-api` INV7 |
| FR3 | Plain, new-tab, modified, middle-click, and surface-delegated navigation use the shared destination decision; a blocked destination does not navigate. | `family:navigation-destinations` FR1, FR3–FR5 |
| FR4 | `isDisabled` keeps the hidden control focusable with `aria-disabled="true"`; Enter, Space, and clicks on it do nothing. A disabled link keeps its link role and name but has no `href` or `target`. The control stays focusable and announced (`spec:AST-085` FR1, FR2, FR5); the card's own click and mouseup handling and the caller's `onMouseUp` are removed, other caller event props still reach the root, and the surface shows no hover or pressed paint (`spec:AST-085` FR6), and renders at half opacity. The disabled state is reflected on the owning target (`architecture:component-theming-surface` INV6). Implementation gap: the `clickable-card` target reflects no disabled state (Current-state impact). | Current source and tests |
| FR5 | While enabled, the root paints the shared hover overlay under a hover-capable pointer and the pressed overlay for mouse and touch presses that would activate the card; a touch press that starts on a nested interactive element shows no pressed paint, since it does not activate the card (the counterpart of FR2's activation rule), while a mouse press there keeps the `:active` paint, and a bordered card's border responds to hover. A press shows at once. Under the touch presentation, its release fades on the theme's exit motion tokens (`--duration-exit`, `--ease-exit`), per `spec:AST-094` FR5; a mouse press keeps the card's desktop press styling. Keyboard focus on the hidden control shows the shared focus outline on the root. | Current source, stories, and `architecture:interaction-modality` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: "A press shows at once" also reads as covering a mouse press, which still fades in over --duration-fast (the browser spec's desktop case asserts that timing while the mouse is down). "A touch press shows at once" matches the code; the same sentence is in SelectableCard FR4 and Thumbnail FR4.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ab1d195: the clause reads "A touch press shows at once" in the three card records.

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.
@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/approve-spec ab1d195

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: CI has not run on this head yet. It runs once the pull request leaves draft and a maintainer adds the ci:run label, since the branch is in a fork.

Under reduced motion, a pressed Button now stays full size, and ClickableCard, SelectableCard and Thumbnail change their hover, press, border and selection colours at once; on a touch screen only opacity still fades, within 200 ms. An engineering owner approved this head's record changes, real-browser checks show the new behaviour against the base, and nothing blocks.

  • Optional follow-up: the card clauses keep a mouse press on the cards' fast CSS fade, which the touch-presentations spec's press rule (FR5, and DEC-21's "instead of the browser's :active") does not carve out; this is already how main behaves, so aligning the two is a separate decision.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — ab1d1956 Deployed Nov 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants