Skip to content

fix(core): Button and the cards follow design:reduced-motion - #8326

Draft
vjeux wants to merge 1633 commits into
meta:mainfrom
vjeux:uikit/c01-reduced-motion
Draft

vjeux wants to merge 1633 commits into
meta:mainfrom
vjeux:uikit/c01-reduced-motion

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review this PR alone: vjeux/astryx@uikit/f2-click-guard...uikit/c01-reduced-motion

User impact

Part of the UIKit-parity work on Astryx's buttons. For people who turn on reduced motion, a pressed Button still shrinks to 98%, and Button's and the cards' hover and press colors still fade, border included. The setting asks for neither.

Problem and solution fit

What led to it: design:reduced-motion became current, and 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 and animates nothing. The cards change at once with a mouse; on a touch screen they cross-fade opacity only, within 200 ms, and their colors, border and selection ring change at once.

Expected behavior and authority

design:reduced-motion DR1 (no scaling), DR2 (a touch screen may cross-fade opacity within 200 ms), DR3 (any other pointer changes at once), DR4 (no theme opt-out); #8645 DEC-3.

Smallest restoration

  • One global "no transitions" rule: touch screens would lose the short cross-fade DR2 allows, which confirms a tap.
  • Chosen: reduced-motion rules on Button's and the cards' own transitions, using the shared policy's values. They're written out because a swizzled component can import only exported paths; a test keeps them equal to the policy.

Tradeoff: the values repeat in each component, held by that test. No API change.

Evidence

  • Size (gzip): Button JS +36 B, CSS +111 B; ClickableCard and SelectableCard about +48 B, CSS +170 B; Thumbnail +58 B, CSS +194 B.

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 fades, within 0.2 s; selecting a SelectableCard changes its border and ring at once.

What was run: a Chromium test under emulated reduced motion reads the computed transforms and transitions. Before this change a pressed Button scaled and the cards ran color transitions; after, it passes. Unit tests fail if any reduced-motion value drifts from the shared policy, and a swizzle test checks that a swizzled Button's imports resolve. These ran locally on this head; GitHub Actions isn't running on this repository right now.

Button.tsx and the cards' overlay also change in #8118; whichever lands second rebases.

Stack: on #8360, which carries #8076 and the gesture foundation.

@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 Oct 28, 2026 8:45pm 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
❓ Needs maintainer input (for maintainers only) Nov 1, 2026, 1:44 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

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

Button (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 712B 712B 0B
Lines of Code 584 596 -
Complexity Very High Very High (83) -
ClickableCard (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 427B 427B 0B
Lines of Code 229 254 -
Complexity High High (19) -
SelectableCard (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 108B 108B 0B
Lines of Code 305 330 -
Complexity Very High Very High (32) -
Thumbnail (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 164B 164B 0B
Lines of Code 355 367 -
Complexity Very High Very High (45) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 56.9KB 57.0KB +152B 12.4KB 12.5KB +100B
core/AppShell 57.9KB 58.0KB +124B 11.6KB 11.8KB +114B
core/AvatarGroupOverflow 39.9KB 40.0KB +125B 6.4KB 6.4KB 0B
core/Banner 47.8KB 48.0KB +157B 9.9KB 10.0KB +112B
core/BaseTypeahead 54.3KB 54.5KB +143B 9.9KB 10.0KB +108B
core/BreadcrumbItem 66.4KB 66.5KB +139B 11.0KB 11.1KB +122B
core/Button 43.3KB 43.4KB +136B 9.2KB 9.3KB +111B
core/Calendar 54.9KB 55.1KB +140B 10.0KB 10.1KB +100B
core/Carousel 50.2KB 50.3KB +116B 10.2KB 10.3KB +108B
core/ChatComposer 60.6KB 60.8KB +137B 10.8KB 10.9KB +109B
core/ChatComposerInput 55.7KB 55.9KB +132B 10.1KB 10.2KB +110B
core/ChatComposerTokenElement 55.7KB 55.9KB +134B 10.1KB 10.2KB +110B
core/ChatDictationButton 46.5KB 46.6KB +140B 9.5KB 9.6KB +109B
core/ChatLayout 48.8KB 49.0KB +139B 9.9KB 10.0KB +111B
core/ChatLayoutScrollButton 46.4KB 46.5KB +119B 9.6KB 9.7KB +109B
core/ChatSendButton 45.8KB 45.9KB +130B 9.2KB 9.3KB +110B
core/CheckboxInput 49.9KB 50.0KB +105B 9.3KB 9.3KB 0B
core/CheckboxListItem 54.0KB 54.1KB +92B 10.5KB 10.5KB 0B
core/ClickableCard 12.8KB 13.0KB +164B 6.9KB 7.1KB +172B
core/CodeBlock 60.5KB 60.7KB +142B 10.7KB 10.8KB +130B
core/Collapsible 11.7KB 11.8KB +113B 4.2KB 4.2KB 0B
core/CommandPalette 50.9KB 51.0KB +104B 11.8KB 11.8KB 0B
core/CommandPaletteItem 8.4KB 8.5KB +110B 3.5KB 3.5KB 0B
core/ComplexSelector 55.8KB 56.0KB +166B 10.6KB 10.7KB +112B
core/ContextMenu 77.7KB 77.9KB +119B 14.6KB 14.8KB +127B
core/ContextMenuItem 16.6KB 16.7KB +117B 5.2KB 5.2KB 0B
core/DateInput 99.9KB 100.0KB +113B 14.8KB 14.9KB +109B
core/DateRangeInput 69.4KB 69.5KB +130B 11.2KB 11.3KB +99B
core/DateTimeInput 110.1KB 110.2KB +104B 15.2KB 15.3KB +105B
core/DialogHeader 48.8KB 48.9KB +134B 9.8KB 9.9KB +113B
core/DrawerHeader 48.9KB 49.1KB +130B 11.2KB 11.3KB +113B
core/DropdownMenu 72.7KB 72.8KB +139B 14.7KB 14.9KB +135B
core/DropdownMenuCheckboxItem 16.1KB 16.2KB +106B 5.6KB 5.6KB 0B
core/DropdownMenuItem 15.4KB 15.5KB +101B 5.2KB 5.2KB 0B
core/DropdownMenuRadioItem 16.2KB 16.3KB +110B 5.6KB 5.6KB 0B
core/DropdownMenuSubMenu 58.1KB 58.2KB +95B 9.5KB 9.5KB 0B
core/FileInput 54.1KB 54.2KB +136B 10.3KB 10.5KB +120B
core/IconButton 43.3KB 43.5KB +149B 9.2KB 9.3KB +111B
core/Item 11.4KB 11.5KB +98B 4.7KB 4.7KB 0B
core/LayerProvider 63.3KB 63.4KB +151B 10.1KB 10.2KB +111B
core/Lightbox 51.2KB 51.3KB +129B 9.8KB 9.9KB +111B
core/Link 43.5KB 43.6KB +112B 7.6KB 7.6KB 0B
core/ListItem 53.5KB 53.6KB +103B 10.5KB 10.5KB 0B
core/Markdown 144.5KB 144.6KB +110B 17.4KB 17.5KB +112B
core/MobileNav 51.0KB 51.1KB +129B 10.1KB 10.2KB +113B
core/MobileNavToggle 46.2KB 46.4KB +137B 9.4KB 9.5KB +109B
core/MoreMenu 72.9KB 73.0KB +135B 14.7KB 14.9KB +135B
core/MultiSelector 76.2KB 76.3KB +152B 14.6KB 14.7KB +110B
core/NumberInput 55.7KB 55.8KB +139B 10.7KB 10.8KB +126B
core/Outline 28.6KB 28.7KB +113B 4.0KB 4.0KB 0B
core/Pagination 87.7KB 87.9KB +136B 15.3KB 15.5KB +115B
core/Popover 49.8KB 50.0KB +118B 12.1KB 12.2KB +109B
core/PowerSearch 155.1KB 155.2KB +120B 18.6KB 18.8KB +131B
core/RadioListItem 52.1KB 52.3KB +111B 9.7KB 9.7KB 0B
core/SegmentedControlItem 8.6KB 8.7KB +116B 3.9KB 3.9KB 0B
core/SelectableCard 12.8KB 12.9KB +160B 7.1KB 7.3KB +172B
core/Selector 79.1KB 79.2KB +117B 15.0KB 15.1KB +129B
core/SelectorOption 14.5KB 14.6KB +97B 5.1KB 5.1KB 0B
core/SideNav 58.0KB 58.1KB +130B 10.6KB 10.7KB +113B
core/SideNavCollapseButton 46.6KB 46.8KB +123B 9.5KB 9.6KB +110B
core/SideNavHeading 54.7KB 54.8KB +128B 10.1KB 10.3KB +116B
core/SideNavItem 54.5KB 54.6KB +131B 10.1KB 10.2KB +116B
core/Step 32.4KB 32.5KB +118B 5.3KB 5.3KB 0B
core/Stepper 47.4KB 47.5KB +140B 9.6KB 9.7KB +108B
core/Switch 48.4KB 48.5KB +111B 9.3KB 9.3KB 0B
core/Tab 9.7KB 9.8KB +119B 4.1KB 4.1KB 0B
core/TabMenu 51.6KB 51.7KB +119B 10.0KB 10.1KB +111B
core/Table 84.0KB 84.1KB +104B 15.1KB 15.2KB +109B
core/TableCell 78.8KB 78.9KB +127B 14.8KB 15.0KB +110B
core/TableHeaderCell 78.6KB 78.7KB +124B 14.8KB 14.9KB +123B
core/TableSelectionToolbar 52.4KB 52.5KB +131B 11.2KB 11.3KB +114B
core/TextInput 53.5KB 53.7KB +133B 10.5KB 10.6KB +129B
core/Thumbnail 50.7KB 50.8KB +166B 11.0KB 11.2KB +195B
core/TimeInput 86.8KB 87.0KB +111B 13.8KB 13.9KB +115B
core/Timestamp 27.5KB 27.5KB 0B 9.5KB 9.7KB +110B
core/Toast 59.8KB 60.0KB +138B 9.7KB 9.8KB +111B
core/ToggleButton 44.1KB 44.3KB +124B 9.3KB 9.4KB +113B
core/Token 29.6KB 29.7KB +111B 4.5KB 4.5KB 0B
core/Tokenizer 64.8KB 65.0KB +153B 11.7KB 11.8KB +115B
core/TopNav 51.9KB 52.0KB +136B 10.4KB 10.5KB +121B
core/TopNavHeading 54.2KB 54.3KB +134B 9.9KB 10.1KB +121B
core/TopNavItem 9.0KB 9.2KB +116B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 54.2KB 54.3KB +152B 10.8KB 10.9KB +105B
core/TopNavMegaMenuItem 9.1KB 9.2KB +119B 4.1KB 4.1KB 0B
core/TopNavMenu 53.6KB 53.7KB +135B 10.1KB 10.2KB +113B
core/TreeList 32.5KB 32.6KB +111B 4.7KB 4.7KB 0B
core/Typeahead 61.5KB 61.6KB +141B 11.3KB 11.4KB +112B

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 34.6KB 34.8KB +246B

Accessibility Audit

Status: 5 accessibility violation(s) found — 5 serious.

Button pattern - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/10 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
Checkbox pattern - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/3 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
ClickableCard - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/5 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
SelectableCard - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/4 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
Thumbnail - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/12 stories · Learn more
    • WCAG: 1.4.3 (Level AA)

Visual Regression

55 of 502 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
RichTextEditor Markdown parity: side by side probe dark 558,840
RichTextEditor Markdown parity: side by side probe light 540,756
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
AppShell Playground neutral dark 65,198
AppShell Playground neutral light 63,788
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 35 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full 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 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.

startCompositorTrace keeps every Display::FrameDisplayed in the browser, and
those events carry no page identity: with a second page animating, a stall
inside the block passed busyWindowProof with presented, crediting the other
page's frames. It now throws when another page is open as it starts or
stops, and says the record is browser-wide (JSDoc and README). Negative
control: another animating page makes it refuse to start, and a page opened
mid-trace makes stop() refuse.
A bare relative ASTRYX_WEBKIT_SHADER_CACHE was mounted as a named volume
instead of a host directory, and an empty one made mkdirSync throw. The
script now resolves a set value with path.resolve and falls back to the
default cache directory on an empty one.
vjeux added 12 commits October 27, 2026 06:50
…e pointer, and no menu observes the pointer query (useMenuPress FR9; spec:AST-094 FR24)

Where the primary pointer is fine, a finger held on a DropdownMenu trigger
keeps the released behavior, whose tolerance is 10 px on each axis: an
(8, 8) settle opened the menu, and the long press's distance rule now
cancels it. And every useMenuPress caller, each menu and sub-menu,
subscribed to (pointer: coarse) though only the trigger reads it, when its
press starts. Both fail until the trigger takes its tolerance at the press.
…rts, and no menu observes the pointer query (spec:AST-094 FR29, FR24; useMenuPress FR9)

Every useMenuPress caller, each menu and sub-menu, subscribed to
(pointer: coarse) through useSyncExternalStore, though only the trigger's
held finger reads it. The trigger now reads the pointer as its press
starts and hands useLongPress that press's tolerance (start's new
tolerance argument): FR29's 13px drift where the primary pointer is
coarse, the long press's own 10px where it is fine, as the released
behavior keeps (on each axis, with meta#8358's measure). A changeset declares
the 13px a finger now gets on phones and tablets.
…entation says

useLongPress documents its tolerance as movement past moveCancelPx on
either axis, a released contract, and ContextMenu's measured hold and the
menu trigger's released behavior keep it. This pull request had changed it
to a distance, which cancels a (9, 9) drift the contract keeps. Fails until
the measure is the documented one again.
…ocumented; only the defaults have one source

useLongPress documents movement past moveCancelPx on either axis, and its
callers keep that: ContextMenu's measured hold is per axis, and the menu
trigger's released behavior keeps the hook's tolerance (useMenuPress FR9).
The defaults, 500ms and 10px, still come from the long-press
recognizer's table, which measures its own tolerance as a distance, as
UIKit's does. The changeset now says what this is: one source for the
numbers, no behavior change; useLongPress.doc.mjs was right all along.
…adow root; a claimed touch's moves are cancelled where fingers come from pointer events

With targets from the composed path, a finger inside an open shadow root
sits below the scroller only along the flat tree, which Node.contains does
not follow: a scroll under it did not take it, and its flick did not arm
the scroll brake. And where the host reads fingers from pointer events
(inputMode 'auto' with no ontouchstart), the blocking touchmove found no
finger for a claimed pan's touch, so its moves went to the page. Three
cases fail until the host compares along the flat tree and finds that
finger; a fourth pins a finger scroll inside a shadow root.
… tree; a claimed touch finds its pointer finger wherever fingers come from pointer events

containsAlongFlatTree walks parentAcrossShadow, so a finger inside an open
shadow root, now that targets come from the composed path, counts as under
a scroller outside it: a scroll there takes it, its flick arms the scroll
brake, and a touch there brakes. cancelIfClaimed matches a touch to its
pointer finger whenever the host reads fingers from pointer events, not
only under inputMode 'pointer-events', so a claimed pan's moves are
cancelled where a browser that exposes no ontouchstart still sends Touch
Events.
… flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8320
…r along the flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8293
…r along the flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8356
…r along the flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8358
…in, as documented; meta#8336: the host compares a finger with a scroller along the flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8360

# Conflicts:
#	packages/core/src/hooks/useLongPress.ts
…handlers type

UseLongPressHandlers.start takes a point only again, as released: the
per-press tolerance useMenuPress's trigger chooses as its press starts
lives on the internal useLongPressDetector and LongPressDetectorHandlers,
which the hooks entry does not export. useLongPress returns the same
detector, typed as before.
@cixzhang cixzhang closed this Oct 28, 2026
@cixzhang cixzhang reopened this Oct 28, 2026
@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf: closed and reopened only to restart this PR's CI on the current workflow after tonight's queue-wide cancel; nothing in the PR changed.

vjeux and others added 12 commits October 28, 2026 10:39
Main merged meta#8045 and meta#8046. The one conflict is utils/pressFeedback.ts:
it takes main's merged meta#8046 text (the finger and button slots, the mouse
and key press) and keeps this pull request's own additions, the one
writer of the press attribute (writePress, paintPress, readPressPaint)
and the header line naming them.
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
Main's button slot, merged with meta#8046, ends a press's release at once
when the next press starts, by clearing the press attribute directly. On
a row another painter holds, that clear erased the hold, which this pull
request's one writer refuses. A mouse press on a held row, released, then
a press elsewhere must leave the row held. Fails until the early end goes
through the one writer.
finish() writes none through writePress, as this pull request's
controller paints, so a held row stays its owner's when the next press
ends a release early, under the finger and the button slots alike.
Both upstream heads are folded onto main 70ea859 (her meta#8045 frame
evidence and meta#8046 coarse press): meta#8446 takes main's harness files, and
meta#8073 carries the press controller onto meta#8046's slots. meta#8076 has no change
of its own in either set of files, so the merge takes theirs with no hand
resolution, and meta#8076 is clean against main again.
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	packages/core/src/utils/pressFeedback.ts
…bute

From the review of meta#8076: the README said paintPress in
utils/pressFeedback.ts is the one writer of data-astryx-press, but the
action-group tracker on main (meta#8346) writes its groups' presses itself.
The sentence now says what holds at this head.
meta#8076's new head carries main 70ea859 with meta#8073's writer of the press
attribute on main's press controller, and its README names both writers.
utils/pressFeedback.ts keeps that controller with this line's own changes
on it: the pointer, drag, scroll and visibility listeners through the
shared input stream (meta#8356) and touchcancel only while the primary pointer
is coarse (meta#8270); the key path stays at the window's bubble phase, so a
Space press the page cancels paints nothing. usePressTracker stays in its
own module, one tracker for a component's life (meta#8315), so meta#8073's
rebuilt-tracker hook and its test are not taken. The harness files are
meta#8076's.
…s, and the action-group tracker writes its own

The header and writePress called it the one writer of the press attribute,
but main's action-group tracker writes its groups' presses itself. They now
say what holds, as meta#8076's gestures README does.
…he press attribute

The release note said every press painter writes data-astryx-press
through one internal writer, but the action-group tracker
(hooks/actionGroupTracker.ts) writes its groups' presses itself, as the
gestures README and utils/pressFeedback.ts already say. The note now says
paintPress writes every press it tracks and the action-group tracker
writes its own.
…r buttons and cards

meta#8360 carries meta#8076, this PR's base, and the press controller as it now
stands; this PR's own files are unchanged.

@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 needs an engineering owner's decision before it can be approved: under Reduce Motion on a touch screen, ClickableCard, SelectableCard and Thumbnail now switch their press and selection colours at once with no cross-fade, which relies on reading design:reduced-motion DR2's in-place value exception (DEC-3), written for continuous values such as a progress fill, as covering colour state changes. An engineering owner records that ruling by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner; otherwise the cards need an opacity cross-fade of 200 ms or less. The Button press-scale fix and the cards' mouse and trackpad fix read correctly against component:Button FR15 and DR1 and DR3. Independently of that decision:

  • Please land the stacked predecessors first, or rebase onto main, so this pull request carries only its own seven-file change instead of about 27,300 lines.
  • In the stack, the gesture host attaches recognizers and press trackers only where the primary pointer is coarse, while current spec:AST-094 FR19 and FR43 require gestures for every pointer (packages/core/src/gestures/GestureHost.ts L14–22).

[Automated review]

Comment on lines +14 to +22
* - Recognizers attach, and the host registers listeners, only while the
* primary pointer is coarse (`spec:AST-094` FR19, FR24; coarsePointer.ts).
* Where it is fine, an attach is recorded but nothing is installed: no
* document, window or element listener, no `touch-action` or other style,
* nothing delivered, so a finger on a touchscreen laptop gets the
* browser's defaults. The host follows the query: turning coarse installs
* it and styles every registration; turning fine cancels every touch in
* flight and removes it all. A development warning names the first attach
* under a fine pointer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Current spec:AST-094 FR19 reads "Recognizers attach for every pointer, with FR43's exceptions", and FR43 "Gestures attach for every pointer, a mouse, a pen, and a finger, not only a coarse primary pointer". This gate, and the development warning further down that cites FR19 for it, keeps every recognizer and press tracker inactive on a desktop. Attach for every pointer with FR43's exceptions (row swipe under a mouse drag, pinch under a pen, screen-edge pan under a mouse, trackpad or pen), keeping FR24's fine-pointer listener budget.

// Reduced motion on a touch screen: only opacity may cross-fade
// (design:reduced-motion DR2), so a selection's ring and border change
// at once.
'@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.

On a touch screen with Reduce Motion this leaves only opacity to transition, and no hover, press or selection state changes opacity, so the selection ring and border colour switch at once. design:reduced-motion DR2 asks for an opacity cross-fade of a state change on a coarse pointer unless it is an in-place value change (DEC-3); whether these colour changes count as one is waiting on an engineering owner's ruling.

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:35

This branch was successfully deployed

1 active deployment
Preview — 9c207a15 Deployed Oct 28, 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