Skip to content

fix(ProgressBar): under reduced motion the fill jumps to a new value - #8614

Open
cixzhang wants to merge 4 commits into
mainfrom
fix/progressbar-reduced-motion-fill
Open

cixzhang wants to merge 4 commits into
mainfrom
fix/progressbar-reduced-motion-fill

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Owner decision D36 (decided): under reduced motion, a progress fill's value change applies at once on any pointer, with no fade and no second fill. This PR does exactly that.

With Reduce Motion on, ProgressBar's determinate fill still grew or shrank to each new value over 410 ms. design:reduced-motion DR1 forbids a resize under reduced motion.

  • With Reduce Motion: the fill's width transition takes no time, so it jumps to the new value, under any pointer.
  • Without Reduce Motion: unchanged.
  • Why not a touch fade: a fade-in remounted the fill on every change and restarted it from opacity 0, so on a fast upload (a step every 50 ms) the filled part never showed above 6 % opacity. Its always-mounted second fill was also a second copy (component-style-authoring INV7).
  • Not in scope: the indeterminate loop (its Reduce Motion policy is a separate decision) and the width → transform change (waiting on a decision about the rounded ends).
  • Test: ProgressBar.reduced-motion.source-build.test.mjs reads the compiled rules: the reduced-motion width transition takes no time, and no reduced-motion rule is scoped to a pointer or fades or animates the fill, so a touch screen and a mouse behave alike. It fails on the previous source and on the touch-fade attempt.

Evidence

Real Chrome (headless, Linux), on this PR's CI Storybook (ProgressBar / Default) beside main's. The value changes from 20 to 90; frames run from the change to about 0.5 s later. Desktop uses a mouse (fine pointer); the phone uses touch (coarse pointer).

Desktop, 1280 px, mouse Phone, 390 px, touch
Reduce Motion off desktop phone
Reduce Motion on desktop, reduce motion phone, reduce motion

Measured fill width after the change:

  • Reduce Motion off: unchanged. Both grow over 0.3 s (605 → 1083 → 1123 px on desktop).
  • Reduce Motion on, mouse and touch alike: main still grows over 0.3 s. This PR is at the new value in the first frame (transition 0s), with no fade.

design:reduced-motion DR1 forbids a resize under reduced motion, and the
determinate fill transitioned its width on every value change. Under
prefers-reduced-motion: reduce the transition now takes no time, so the
fill jumps. The indeterminate loop is unchanged (its policy is a separate
decision). A source-build test reads the compiled rules and fails on the
previous source.
@vercel

vercel Bot commented Oct 26, 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 27, 2026 3:47am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 27, 2026, 4:10 AM UTC

@github-actions

github-actions Bot commented Oct 26, 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

ProgressBar (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 455B 455B 0B
Lines of Code 387 390 -
Complexity High High (29) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/ProgressBar 7.7KB 7.7KB +7B 6.6KB 6.6KB +17B

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.3KB 34.3KB 0B

Accessibility Audit

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

ProgressBar - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 2/18 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
Progressbar - 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/1 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.

With Reduce Motion on, ProgressBar's fill now takes its new value at once with a mouse, trackpad or keyboard, which matches design:reduced-motion DR1 and DR3, and the new test fails on the previous source as described. Approval waits on one decision for touch screens: the same rule makes the fill jump on a coarse pointer, while DR2 currently asks for an opacity-only cross-fade of 200 ms or less there and lists "a change with no cross-fade" as a failure. Any engineering owner, the author included, needs to decide whether DR2 should allow an immediate change for an in-place value change like this one, and amend design:reduced-motion to say so, or whether the fill should cross-fade on touch, which this PR would then add.

[Automated review]

// (design:reduced-motion DR1).
transitionDuration: {
default: durationVars['--duration-medium'],
'@media (prefers-reduced-motion: reduce)': '0s',

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 Reduce Motion this 0s also applies on a coarse (touch) pointer, so the fill jumps there too. design:reduced-motion DR2 currently asks for an opacity-only cross-fade of 200 ms or less on a coarse pointer and lists "a change with no cross-fade" as a failure, so this line waits on an engineering owner's decision: amend DR2 for in-place value changes, or add a touch cross-fade here.

…th fades in

design:reduced-motion DR2 asks a touch screen for an opacity-only fade of
200 ms or less, not a jump. Under reduced motion on a coarse pointer the
transitioning fill steps aside for a second fill keyed by the value: each
change remounts it at the new length and it fades in (200 ms, easeInOut).
With a fine pointer the fill still changes at once (DR3), and nothing
animates width or transform under reduced motion (DR1). ProgressBar stays
presentational: it remembers no previous value, so the old length does not
fade out separately. The source-build test and a render test (the keyed
fill remounts, the transitioning one stays) fail on the previous head.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 27, 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.

With Reduce Motion on, ProgressBar's fill now changes at once with a mouse, trackpad or keyboard, which matches design:reduced-motion DR1 and DR3, but the touch-screen treatment can't be approved yet. It waits on an engineering owner's decision, which the author can make as one: either amend DR2 so an in-place value change like this one also changes at once on touch, or record an architecture:component-style-authoring INV7 exception for the second, always-mounted fill a touch cross-fade needs, and approve that record change here with /approve-spec <full head SHA>. Separately, the touch fade restarts from opacity 0 on every value change, so the filled part blanks each time and, with the value stepping every 50 ms, the fill never rises above 0.06 opacity; that needs fixing under either outcome.

[Automated review]

style={{width: `${percentage}%`}}
/>
<div
key={percentage}

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 coarse pointer under Reduce Motion this key remounts the fill on every value change, and fillFadeIn (fill mode both) restarts it from opacity 0, so the part already filled blanks each time. With the value stepping every 50 ms the fill never rose above 0.06 opacity, behind a label that reads the real value. Whatever is decided for the touch treatment, the filled length needs to stay visible through a change, or the change should land at once.

)}
style={{width: `${percentage}%`}}
/>
<div

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 second fill stays mounted in every determinate bar and is display: none everywhere except touch with Reduce Motion, where the first one is hidden instead (the Default story goes from 111 to 112 elements under both pointers). architecture:component-style-authoring INV7 and spec:AST-017 FR60 allow a second copy only transiently, for exits, presentation switches, lifts and sheet handoffs, or through an owner exception recorded with before and after counts; none is recorded yet.

… any pointer

The touch fade-in remounted the fill on every change and restarted it from
opacity 0, so on a fast upload the filled part never showed (6% opacity at
a 50 ms step), and its always-mounted second fill was a second copy
(component-style-authoring INV7). This returns to the first head's
behavior: under reduced motion the fill changes at once on any pointer,
pending the owner's decision on recording that for in-place value changes
in design:reduced-motion DR2.

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

With Reduce Motion on, ProgressBar's fill now takes its new value at once with a mouse, trackpad or keyboard, which matches design:reduced-motion DR1 and DR3, and the new test fails on the previous source as described. On a touch screen the fill jumps too, while DR2 currently asks each change on a coarse pointer to cross-fade and lists "a change with no cross-fade" as a failure. Approval waits on an engineering owner deciding that, and the author can act as one: either amend DR2 so an in-place value change like this fill's length lands at once on any pointer and record it here by commenting /approve-spec <full head SHA>, or keep DR2 and have this PR add a touch cross-fade for the fill. If DR2 is amended, nothing else in this PR needs to change.

[Automated review]

@cixzhang

Copy link
Copy Markdown
Contributor Author

Posted by Cindy's agent on her behalf.

Cindy's decision for progress under Reduce Motion: in-place value changes, such as a progress fill's length, apply at once on any pointer. There's no cross-fade and no extra element, so progress stays readable at any update rate. design:reduced-motion DR2 gets a note excluding in-place value changes from the touch cross-fade, and this PR conforms: the fill changes at once under Reduce Motion with a touch pointer or a mouse.

…een and with a mouse alike

D36: under reduced motion a progress fill's value change applies at once on
any pointer, with no fade and no second fill. The source-build test now
pins that no reduced-motion rule is scoped to a pointer and none fades or
animates the fill; it fails on the touch-fade head (e6ed676).

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

With Reduce Motion on, ProgressBar's fill now lands on its new value at once, which matches design:reduced-motion DR1 and DR3, and the new test fails on the previous source as described. On a touch screen it lands at once too, but DR2 on main still asks each change on a coarse pointer to cross-fade, so approval needs that record updated for in-place value changes, as #8645 proposes. Once #8645 is merged nothing else here needs to change; alternatively this PR can carry the same DR2 note, and an engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, which the author may do as an engineering owner.

[Automated review]

Comment on lines +44 to +47
const reduced = rules.filter(rule =>
rule.startsWith('@media(prefers-reduced-motion:reduce)'),
);
expect(reduced.filter(rule => rule.includes('pointer'))).toEqual([]);

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: this filter keeps only rules that start with @media(prefers-reduced-motion:reduce). A touch rule written @media (pointer: coarse) and (prefers-reduced-motion: reduce) compiles to @media(pointer:coarse)and(prefers-reduced-motion:reduce){…} and never reaches the pointer or fade checks: the second commit's touch fade written that way passes all three tests. Filtering with rule.includes('prefers-reduced-motion:reduce') would catch either order.

This branch was successfully deployed

1 active deployment
Preview — 13b047dd Deployed Oct 27, 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.

1 participant