Skip to content

feat(ProgressBar): under reduced motion the indeterminate bar pulses in place - #8239

Draft
vjeux wants to merge 4 commits into
meta:mainfrom
vjeux:uikit/c07-progressbar-indeterminate
Draft

vjeux wants to merge 4 commits into
meta:mainfrom
vjeux:uikit/c07-progressbar-indeterminate

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stacks on #8225; only the last commit is new here.

User impact

With reduced motion on, an indeterminate ProgressBar still slides a 40% segment across the track, slowed to 3 s a pass. design:reduced-motion DR1 says nothing should travel there, so the bar now stays put: on every pointer its fill spans the whole track and only its opacity pulses, from full down to 0.84 and back, 2 s each way.

Options and tradeoffs:

  • Keep the slower slide: it still travels.
  • A still bar: nothing tells the user work is happening.
  • iOS Safari's pulse (1 s between 0.30 and 0.60): below 0.84, some tones drop under 3:1 against the track.
  • A slow pulse with a 0.84 floor (this PR): every tone that reaches 3:1 against the track at full opacity still does at the floor.

Visual authority

  • Existing component/module DD row reused or amended.
  • Durable component-local intent added to the optional Design decisions table by a team design owner.
  • Correction already settled by current family, design, theme, or objective accessibility authority.
  • New shared or cross-component direction settled in docs/design/.

Settled by component:ProgressBar FR6, AR4 and DEC-2: the pulse plays the progress.reducedMotionPulseMs role on timing.curves.move, which tempo does not scale.

Before and after

Reduced motion, Chromium, one bar seeked through a cycle; before (main) then after:

Before: the 40% segment still slides across the track, 3 s a pass After: a full-width bar that stays put and only dims to 0.84 and back, 2 s each way

State matrix

  • Reduced motion, a mouse or a finger: full width, pulsing; nothing moves.
  • No reduced motion: the 40% slide, unchanged.
  • A theme tempo of 1.5: still 2 s each way.
  • Light and dark default themes, body and surface: accent, success and error keep 3:1 at the floor (lowest: success in dark on the surface, 3.007:1); warning and neutral in light are under 3:1 even at full opacity.

Writing rubric results

component:ProgressBar (FR6's gap lines, the FR6 and AR4 verification rows) and the generated role table: R1–R6 none; the rows name the new tests.

Scope

  • Appearance is the primary intent.
  • Behavior, API, interaction model, and unrelated layout changes were removed or split.
  • The evidence shows the changed pixels actively running, not only source or computed styles.
  • Public text and artifacts contain no internal Meta context.

Testing

Local gates at e1e83a3: the core unit suite (17,869); ProgressBar browser specs 43/43, and ×3 on two pinned cores 129/129; typecheck, lint, the changeset, knowledge and role-table checks pass.

@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 2, 2026 9:34am 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 2, 2026, 9:49 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
@github-actions

github-actions Bot commented Oct 24, 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 ProgressBar touch change needs an engineering-owner decision before it can be approved: it rests only on the draft spec:AST-094, so an engineering owner, the author included, can record approval of that direction (the touch indeterminate sweep, its Reduce Motion pulse and its timing outside the theme's motion tokens) by commenting /approve-spec <full head SHA>, or point to another governing spec. Whatever that decision, two changes are needed:

  • Land the stacked predecessors first (or retarget) so this pull request carries only its own 461-line commit instead of 16,939 authored lines.
  • Under a coarse pointer, a value that reverses to the one it just left animates in half the stated time (400 ms instead of 800 ms), because CSS shortens reversed transitions.

[Automated review]

transitionProperty: 'transform',
transitionDuration: {
default: durationVars['--duration-medium'],
'@media (pointer: coarse)': 'var(--_progress-bar-duration, 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 a coarse pointer, a change that reverses to the value it just left runs at half the duration written here. CSS shortens a transition that reverses to its own start value, so 10 → 90 paused at 50% and sent back to 10 runs 400 ms although --_progress-bar-duration is 800 ms (433 ms when reversed live at 52%); a retarget to any other value keeps its full duration. Keep reversals off the browser's reversing path, or document that they follow the drawn distance, and add the case to ProgressBarTouch.a11y.chromium.spec.ts.

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 is not in this head or on main (it is proposed in open #8003), so FR7 asks owners to amend text that is not in the repository. Land AST-090 first, or restate FR7 against records present at the head.

@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 ProgressBar touch change needs an engineering-owner decision before it can be approved: it rests only on the draft spec:AST-094, so an engineering owner (the author included, if they are one) can record approval of that direction (the touch sweep, its trackless Reduce Motion pulse and its timing outside the theme's motion tokens) by commenting /approve-spec <full head SHA>, or point to another governing spec. Whatever that decision, these changes are needed:

  • trackIndeterminate sets default: null, which removes the grey track from every indeterminate ProgressBar, with a mouse too; keep the default colour in the override and test the mouse path.
  • Land the stacked predecessors first (or retarget) so this pull request carries only its own 500-line commit instead of 16,976 authored lines.
  • Under a coarse pointer, a value that reverses to the one it just left animates in half the stated time (400 ms instead of 800 ms).
  • AST-094 FR7 amends spec:AST-090, which is not in the repository.

[Automated review]

// fill's rules under it; the compiled CSS kept both.)
trackIndeterminate: {
backgroundColor: {
default: null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

stylex.props() merges by property, so this backgroundColor (a null default plus one media key) replaces track.backgroundColor whole: every indeterminate track loses --color-background-muted, with a mouse and with a finger, not only under coarse pointer plus Reduce Motion. On CI's static Storybook build of this head the Indeterminate story's track computes to rgba(0, 0, 0, 0) with a mouse, against rgb(212, 212, 212) on the base. Keeping the default in the override (default: colorVars['--color-background-muted']) and asserting the track colour on the mouse and touch-sweep paths would fix and guard it.

@github-actions github-actions Bot removed the needs:design-review Affects visuals — Design should review 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 ProgressBar change needs an engineering-owner decision before it can be approved: it rests only on the draft spec:AST-094, so an engineering owner (the author included, if they are one) can record approval of that direction for the iOS bar (the quarter-track sweep, the trackless Reduce Motion pulse and its theme-retunable timing) by commenting /approve-spec <full head SHA>, or point to another governing spec. Whatever that decision, these changes are needed:

  • trackIndeterminate keeps default: null, which removes the grey track from every indeterminate ProgressBar, with a mouse and on Android too; keep the track colour as the default and test those paths.
  • Land the stacked predecessors first (or retarget) so this pull request carries only its own two commits instead of 19,658 authored lines.
  • The iOS gate infers the engine from -webkit-touch-callout, which spec:AST-013 FR6 does not allow, and the theme platform condition is outside spec:AST-012 FR2's closed vocabulary, so each needs its record amended first or has to go.
  • AST-094 FR4 and DEC-1 say touch motion is not themable while the new theme motion roles retune it, the motion docs advertise roles no component reads, and FR7 amends spec:AST-090, which is not in the repository.
  • On iOS a fill that reverses to its previous value runs at half its written duration, lint and test-node (1/4) fail on a missing @astryxdesign/cli changeset and a stale docs golden, and the description needs AST-094's writing-rubric results.

[Automated review]

default: durationVars['--duration-medium'],
'@media (pointer: coarse)': {
'@supports (-webkit-touch-callout: none)':
'calc(var(--_progress-bar-change, 0) * var(--motion-progress-progress-view-whole-track-ms, 1000ms))',

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 iOS a value that reverses to the one it just left runs at half this duration: CSS shortens a transition that reverses to its own start value. With the iOS gate emulated in Chromium, 10 → 90 held halfway and sent back to 10 runs 400 ms while this computes 0.8 s; a retarget to 30 keeps its 600 ms. Keep reversals off the browser's reversing path, or document that they follow the drawn distance, and add the case to ProgressBarTouch.a11y.chromium.spec.ts.

'pointer',
'contrast',
'motion',
'platform',

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-012 FR2 closes the when vocabulary to width, pointer, contrast and motion: "Future environment fields require explicit additions to this closed vocabulary." platform needs that amendment first (architecture:theme-authoring-contract also says an adaptation change preserves the fixed condition vocabulary), so it would be easier to split it out with the amendment.

* The engine is WebKit on iOS or iPadOS: the only one that implements
* `-webkit-touch-callout`. With TOUCH_QUERY, the iOS idiom.
*/
export const IOS_SUPPORTS_QUERY = '(-webkit-touch-callout: none)';

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-013 FR6: "Components detect the API or CSS capability they use rather than inferring it from a browser name. A narrow engine workaround is allowed only for a reproduced defect with a regression fixture and a review or removal condition." This query is read only to tell iOS WebKit from every other engine, so the iOS idiom built on it needs FR6's owner to rule before it ships.

measures (0.55 for a scroll view and a sheet), and a release MUST pick its
rest point by projecting its velocity the way its UIKit counterpart does:
with a deceleration rate, or with a measured projection time.
- **FR4 — Physics are not themable in touch presentations.** A 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.

FR4, and DEC-1's "Rejected: tuning touch motion per theme", say touch motion is not themable, but this pull request adds defineTheme({motion: {springs, durations, curves}}) and ProgressBar and Spinner read --motion-* role variables a theme can set. Bring FR4 and DEC-1 in line with the roles, or drop the roles, before owners rule on this record.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
content: [
{
type: 'prose',
text: 'On a touch screen, sheets, alerts, menus, navigation and scrolling play the springs, durations and curves measured from iOS. Each value is a motion role named by its table path, such as `springs.sheetPresent` or `timing.curves.easeInOut`. A theme retunes a role through `motion.springs`, `motion.durations` or `motion.curves`, at its root or in an adaptation rule; a role it leaves alone keeps its measured default.',

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 says sheets, alerts, menus, navigation and scrolling play these roles, but at this head only four roles have a consumer (the three progress.* durations in ProgressBar and progress.activityIndicator.cycleMs in Spinner), so the springs.sheetPresent example below changes nothing. Document only roles a component reads, and reject role ids no table registers instead of accepting them silently.

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

Approval waits on an engineering owner's decision on draft spec:AST-094 for the iOS indeterminate bar, including its iOS engine gate, which spec:AST-013 FR6 does not allow today, and its theme-retunable timing, which the draft's own FR4 rules out. An engineering owner records that decision by commenting /approve-spec b7432c040517a75d25a5827fb880aecf97c2425b on this pull request, and the author may do so as an engineering owner.

  • Restore the indeterminate track, which the null default at ProgressBar.tsx:281-290 removes with a mouse and on Android too.
  • Rebase onto the stacked predecessors so this pull request carries only its own commits, drop when.platform or amend spec:AST-012 FR2, fix the halved iOS reversal, and keep the motion docs to roles that have a reader.

[Automated review]

// with Reduce Motion the track's own color steps aside for the pulse.
trackIndeterminate: {
backgroundColor: {
default: null,

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 null default replaces the track's --color-background-muted on every indeterminate bar, so the track renders transparent with a mouse, on Android and on iOS without Reduce Motion (the base paints rgb(212, 212, 212)). Give both defaults the track colour and keep transparent only under the gated Reduce Motion branch, then assert the track colour on the mouse and Android paths.

default: durationVars['--duration-medium'],
'@media (pointer: coarse)': {
'@supports (-webkit-touch-callout: none)':
'calc(var(--_progress-bar-change, 0) * var(--motion-progress-progress-view-whole-track-ms, 1000ms))',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A move reversed halfway (10 to 90, back to 10 when drawn at 50%) is specified as 0.8 s here, but CSS Transitions shortens a transition that reverses to its start, so Chromium runs it in 400 ms, half what progressView.wholeTrackMs documents. Start the reversal as a fresh transition or restate the table, and add the case to the touch spec.

* is everything else. Pair `ios` with `pointer: 'coarse'` for a touch
* profile, such as touch motion tuned per platform.
*/
platform?: 'ios' | 'other';

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-012 FR2 closes the when vocabulary to width, pointer, contrast and motion and says future fields need explicit additions to it; no theme in the repository sets platform, only the motion doc's example. Remove it, or amend AST-012 FR2 in its own pull request.

* The engine is WebKit on iOS or iPadOS: the only one that implements
* `-webkit-touch-callout`. With TOUCH_QUERY, the iOS idiom.
*/
export const IOS_SUPPORTS_QUERY = '(-webkit-touch-callout: none)';

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 query identifies the engine to choose a presentation; spec:AST-013 FR6 allows an engine test only as a workaround for a reproduced defect with a regression fixture and a removal condition, so it needs an owner decision: amend FR6, or key the presentation on a capability.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
content: [
{
type: 'prose',
text: 'On a touch screen, sheets, alerts, menus, navigation and scrolling play the springs, durations and curves measured from iOS. Each value is a motion role named by its table path, such as `springs.sheetPresent` or `timing.curves.easeInOut`. A theme retunes a role through `motion.springs`, `motion.durations` or `motion.curves`, at its root or in an adaptation rule; a role it leaves alone keeps its measured default.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

At this head only the three ProgressBar durations and Spinner's cycle read a motion role, so sheets, alerts, menus, navigation and scrolling do not follow these roles yet, and the example's springs.sheetPresent and springs.navigationPush change nothing. Document only roles that have a reader, or mark the rest as not yet in use.

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

Approval waits on an engineering owner's decision about draft AST-094. That decision covers the iOS-only engine gate (current AST-013 FR6 does not allow it), the trackless Reduce Motion pulse, and theme-retunable touch roles, which AST-094 FR4 and DEC-1 forbid as written. An engineering owner records it by commenting /approve-spec <full head SHA> on this pull request, and you may do so yourself if you are an engineering owner. Whatever the decision, these still need changes:

  • With Reduce Motion on iOS the pulsing bar reaches only 1.52:1 to 2.42:1 against the page, below the 3:1 non-text contrast AST-020 requires.
  • Rebase onto the stacked predecessors so the pull request carries only its own four commits, about 640 of its 19,658 authored lines.
  • Drop when.platform or amend AST-012 FR2 first, derive the Spinner spoke delays from the cycle role, keep a reversed iOS move at its documented duration, document only motion roles something reads, and fix the citation of the missing AST-090.

[Automated review]

'@media (pointer: coarse)': {
'@supports (-webkit-touch-callout: none)': {
default: colorVars['--color-background-muted'],
'@media (prefers-reduced-motion: reduce)': 'transparent',

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 iOS this removes the track, so the pulsing fill (accent at 0.3 to 0.6 opacity) reaches only 1.52:1 to 2.42:1 against the light page, below the 3:1 non-text contrast that AST-020 requires for a meaningful state graphic. Pulsing at 0.74 opacity or more, or keeping the track and the 3 s slide that a mouse and Android keep, would hold 3:1.

default: durationVars['--duration-medium'],
'@media (pointer: coarse)': {
'@supports (-webkit-touch-callout: none)':
'calc(var(--_progress-bar-change, 0) * var(--motion-progress-progress-view-whole-track-ms, 1000ms))',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A move sent back to its starting value runs at half this duration, because CSS shortens a transition that reverses to its start. With the iOS gate emulated, 10 → 90 takes 800 ms; sent back to 10 halfway, it takes 400 ms, while 90 → 30 from the same point takes the documented 600 ms. A reversal test, plus a path the reversing rule does not shorten (or a documented exception in progress.ts), would close this.

Comment thread packages/core/src/Spinner/Spinner.tsx Outdated
// spoke partway through its cycle, so every spoke is in step from the first
// frame.
const spokeStyles = stylex.create({
s0: {animationDelay: '-600ms'},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These delays are sized for the 800 ms default, but the duration reads --motion-progress-activity-indicator-cycle-ms, which themes are invited to retune. At 1600 ms the bright head sits on one spoke for 800 ms and then steps 0, 7, 0, 1…. Deriving each delay from the same variable, for example calc(var(--motion-progress-activity-indicator-cycle-ms, 800ms) * -6 / 8) here, keeps the order at any cycle.

* is everything else. Pair `ios` with `pointer: 'coarse'` for a touch
* profile, such as touch motion tuned per platform.
*/
platform?: 'ios' | 'other';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AST-012 FR2 closes the condition vocabulary to width, pointer, contrast and motion, and says future environment fields need an explicit addition to it. This adds platform without amending FR2, and nothing in this pull request reads it. Dropping it here, or amending AST-012 first, resolves the contradiction.

* The engine is WebKit on iOS or iPadOS: the only one that implements
* `-webkit-touch-callout`. With TOUCH_QUERY, the iOS idiom.
*/
export const IOS_SUPPORTS_QUERY = '(-webkit-touch-callout: none)';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AST-013 FR6 asks components to detect the capability they use rather than infer the engine, and allows an engine workaround only for a reproduced defect with a regression fixture and a removal condition. Here the property identifies iOS WebKit in order to choose a presentation, so it needs an engineering owner's decision (AST-094 approved together with an FR6 amendment) before it can land.

);
}
const registered = motionRole(id);
if (registered !== undefined && registered.kind !== kind) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A well-formed id that names no registered role passes this check and writes a variable nothing reads; the error text even suggests sheet.present, which no table registers. Rejecting unregistered ids would catch a theme author's typo.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
content: [
{
type: 'prose',
text: 'On a touch screen, sheets, alerts, menus, navigation and scrolling play the springs, durations and curves measured from iOS. Each value is a motion role named by its table path, such as `springs.sheetPresent` or `timing.curves.easeInOut`. A theme retunes a role through `motion.springs`, `motion.durations` or `motion.curves`, at its root or in an adaptation rule; a role it leaves alone keeps its measured default.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Only four of the 50 roles have a reader in this pull request: three in ProgressBar and one in Spinner. Sheets, alerts, menus, navigation and scrolling do not play these roles yet, so this sentence and the example's springs.sheetPresent and springs.navigationPush promise effects a theme will not see.

cixzhang added a commit that referenced this pull request Oct 24, 2026
…ers change at once (#8272)

## Exact decision needed

What a person who prefers reduced motion sees when a core component animates a change, on touch and on other pointers, and whether a component may keep its motion.

## Existing authority searched

- `design:motion` DR5 (draft): every animated transition has an immediate or minimally moving form. Draft, so not authority.
- `spec:AST-013` FR13: reduced-motion behavior is unchanged by server-rendered entrances; it defines none.
- `spec:AST-012` FR2: `motion: reduce` and `pointer: coarse/fine` are the adaptation conditions.
- No current web record defines component behavior under reduced motion.
- Search terms: reduced motion, prefers-reduced-motion, cross-fade, Reduce Motion, motion DR5, spring, travel. Overlapping open pull requests: #7367 (draft `design:motion` DEC-2, hover entrances; no reduced-motion rule), #8214 (layer fade under a finger with Reduce Motion; conforms once its fade is 200 ms or less), #8052 (AST-094 FR8 carries the matching touch rule), #8211, #8239, #8253 (touch indicator and placeholder work; continuous indicators are outside this record).

## Decision

New current record `design:reduced-motion`, matching iOS Reduce Motion with Prefer Cross-Fade:

- DR1: under `prefers-reduced-motion: reduce`, a core component's entrances, exits, presentation switches, and state changes never move, scale, resize, or spring.
- DR2: on a coarse pointer, they cross-fade: opacity only, 200 ms or less.
- DR3: on any other pointer, they complete at once.
- DR4: no core component, prop, or theme value keeps motion under reduced motion.

`spec:AST-057` FR5 names that a swiped row's slide out, spring back, and hold-out follow DR2 under reduced motion. The draft `design:motion` keeps its other rules unapproved; its DR5 points to the new record. A separate record keeps the promotion to this one rule, as `docs/design/README.md` advises for a different approval lifecycle.

## Rejected alternatives

- A per-component setting that keeps full motion under reduced motion.

## Claim scope and non-goals

Only transitions under reduced motion. Motion without the preference, durations and easing, tokens, continuous indicators such as a spinner, and content the person drags with their own finger are outside this record.

## Evidence and implementation relationship

Layers that open under a finger with Reduce Motion appear in one frame today, where iOS cross-fades. The open fix #8214 makes them fade under a coarse pointer and stay immediate under a mouse, which conforms to DR2 and DR3 once its fade is 200 ms or less. The open draft #7367 (`design:motion` DEC-2, hover-summoned layers) does not touch reduced motion. The AST-094 revision in #8052 carries the matching touch rule. No implementation is included.

## Writing rubric results

- R1 narration: none.
- R2 research in the record: none.
- R3 evidence naming a change: none; verification names browser states.
- R4 rejected alternatives over one line or outside a decision: one, inside DEC-1, one line.
- R5 pull request or issue references remaining: none.
- R6 ruled open questions still listing options: none.

## Scope

- [x] This PR contains specification records only.
- [x] It records observable behavior and preserves equivalent internal implementations.
- [x] Any named internal mechanism is intentionally public, names its dependent caller/system, and explains why an equivalent implementation would not satisfy the contract.
- [x] Verification states the evidence layer and failure signal without prescribing CI job/workflow topology.
- [x] It records one intentional durable decision, not a rescue for a separable tagalong.
- [x] The canonical knowledge owner, affected current claims, and overlapping open work are named; any ownership collision is resolved or called out for human decision.
- [x] Adjacent API, visual, accessibility, composition, and implementation facts remain outside scope unless this exact decision depends on them.
- [x] Public text and artifacts contain no internal Meta context.

## Validation

- `pnpm check:knowledge` (CI lint)
- Prettier
- `git diff --check`
@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:

  • Touch gating. core/src/ProgressBar/ProgressBar.tsx:287 writes its own (pointer: coarse) check. This conflicts with AST-012 FR2 (touch is resolved once, through the closed adaptation vocabulary) and public-component-api INV16. Resolve through the shared touch resolver key so behavior matches the spec. If this behavior genuinely can't be expressed through the resolver, please raise it here as a spec question instead of shipping a separate check.
  • Reduced motion. core/src/ProgressBar/ProgressBar.tsx:290 handles reduced motion locally. Per design:reduced-motion (DR1–DR4), the animator is the one place that resolves it (touch cross-fades in 200 ms or less with no travel, a fine pointer changes at once), and core has no opt-out, so reducedMotion: 'keep' should be removed.

This is about where the behavior lives, not about the PR's intent.

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: one implementation.

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

Approval needs an engineering owner to approve or identify the governing spec for the iOS Safari look of ProgressBar's indeterminate bar, which today rests only on draft spec:AST-094: a 25% linear sweep, a Reduce Motion pulse with no track, whether it applies on iOS only or on every touch screen, and whether themes may retune its timing. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Whatever that decision, please rebase onto the landed stack so this diff is the ProgressBar change alone, and fix these:

  • Use the shared compact definition from spec:AST-012 FR9 instead of 744 px, which opens adaptive menus as popovers on 744–767 px touch screens.
  • Key the touch styles on the shared touch resolver instead of -webkit-touch-callout, as spec:AST-013 FR6 and the owner comment ask.
  • Drop when.platform, the theme-settable reduced-motion fade and reducedMotion: 'keep', which spec:AST-012 FR2 and the reduced-motion design record rule out.
  • Make the motion docs describe only roles a component reads, reject unknown role ids, and fix AST-094's citation of the missing AST-090.

[Automated review]

export const TOUCH_QUERY = '(pointer: coarse)';

/** The viewport is compact: layout behaviours. */
export const COMPACT_QUERY = '(width < 744px), (height < 500px)';

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-012 FR9 makes a screen compact below the effective md point (768 px by default) and says no resolver keeps size numbers of its own; on a 750 px touch screen this opens adaptive menus as popovers. main exports compactDefinition() / DEFAULT_COMPACT_DEFINITION for this.

* The engine is WebKit on iOS or iPadOS: the only one that implements
* `-webkit-touch-callout`. With TOUCH_QUERY, the iOS idiom.
*/
export const IOS_SUPPORTS_QUERY = '(-webkit-touch-callout: none)';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

-webkit-touch-callout stands in for "iOS" here, which spec:AST-013 FR6 allows only for a reproduced engine defect with a fixture and a removal condition; the engineering owner asked for the shared touch key instead.

* is everything else. Pair `ios` with `pointer: 'coarse'` for a touch
* profile, such as touch motion tuned per platform.
*/
platform?: 'ios' | 'other';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

platform is not in spec:AST-012 FR2's closed when vocabulary (width, height, pointer, contrast, motion), which requires an explicit addition to the record first.

* Cross-Fade Transitions on (opacity CABasicAnimation 0.5 s,
* (0.42, 0, 0.58, 1)), Oct 23 2026
*/
reducedMotionCrossFadeMs: 500,

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 a theme role this lets a theme lengthen the reduced-motion fade, and its 500 ms default exceeds the 200 ms design:reduced-motion DR2 allows on touch; DR4 says theme values offer no way to keep a longer animation under reduced motion.

`${location}: a motion role is named by its table path, such as "sheet.present" or "springs.navigationPush".`,
);
}
const registered = motionRole(id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

An id the registry does not know passes through and emits a variable nothing reads (the test expects 'sheet.present' to become --motion-sheet-present), so a typo in a theme fails silently.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
content: [
{
type: 'prose',
text: 'On a touch screen, sheets, alerts, menus, navigation and scrolling play the springs, durations and curves measured from iOS. Each value is a motion role named by its table path, such as `springs.sheetPresent` or `timing.curves.easeInOut`. A theme retunes a role through `motion.springs`, `motion.durations` or `motion.curves`, at its root or in an adaptation rule; a role it leaves alone keeps its measured default.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

At this head only ProgressBar and Spinner read a --motion-* variable and no component calls the animator, so retuning springs.sheetPresent or springs.navigationPush as the example shows changes nothing.

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 is not on main or in this pull request, so this amendment cannot be checked.

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

Approval is on hold until an engineering owner settles the governing spec for ProgressBar's touch presentation, which no current record states: the 25% linear sweep, the trackless 0.74–1 Reduce Motion pulse and its departure from Safari's measured 0.3–0.6 (spec:AST-094 leaves each component's presentation to its own record, and component:ProgressBar is still a draft). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Whatever the decision, these changes are still needed:

  • Rebase onto main once #8076, #8196 and #8225 land, so the diff (21,897 authored lines today) holds only this branch's commits.
  • Mirror the touch sweep on :dir(rtl), as the determinate fill already does, so the segment stays on the track in a left-to-right region of a right-to-left page.
  • Read the sweep, pulse and fill timing through motion roles, since spec:AST-094 FR4 says a component “MUST NOT play a literal”.
  • In the carried code, stop the forced-colors fill animating width under a finger (FR5), bring the gesture host to FR18 and FR20, and add real-device iOS Safari and Android Chrome recordings for the touch claims.

[Automated review]

},
transitionProperty: {
default: 'transform',
'@media (forced-colors: active)': 'width',

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 forced colors with a coarse pointer this switches the fill's transition to width, while the touch key still gives it calc(var(--_progress-bar-change, 0) * 1000ms), linear (lines 387-398). On a touch screen with a contrast theme, a value change then lays out every frame and stalls with the main thread. spec:AST-094 FR5 says a touch presentation's animation “MUST NOT animate layout properties”. Landing the value at once under forced colors (transitionDuration: '0s' there) would conform; this arrives with #8196's fix, so it belongs there.

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

Approval is on hold until an engineering owner settles the governing spec for ProgressBar's touch indeterminate bar, which no current record states: Safari's 25% linear sweep, the trackless 0.74–1 Reduce Motion pulse (Safari measures 0.30–0.60), and whether its timing must be AST-094 FR4 motion roles rather than StyleX constants. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, or by naming the record that should state it, and the author may do so as an engineering owner. Either way, please:

  • Rebase or retarget onto #8225 so this pull request carries its own 699 changed lines instead of 19,100.
  • Key the touch sweep's RTL keyframes on :dir(rtl), because ProgressBar.tsx:438 runs the segment off the track in a left-to-right region of a right-to-left page.
  • Add real-device recordings for the iOS Safari and Android Chrome claims, as AST-094's platform support section requires.

[Automated review]

':is([dir="rtl"] *)': indeterminateSlideRtl,
'@media (pointer: coarse)': {
default: touchSweep,
':is([dir="rtl"] *)': touchSweepRtl,

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 key matches any RTL ancestor, so a bar in a left-to-right region of a right-to-left page plays the right-to-left sweep from the left edge and leaves the track: 89.5, 55.9, 22.4 and 0 px stay visible at 0, 125, 250 and 500 ms, and the bar is empty from 333 to 1,667 ms of each cycle. Keying it on :dir(rtl), as the determinate fill does at line 356, follows the bar's own direction (design:bidirectionality DR3).

Comment on lines +25 to +37
export const progressBarTouchConsts = stylex.defineConsts({
/**
* UIProgressView's setProgress(_:animated: true) duration for a change of
* the whole track; a smaller change takes its share of it.
*/
wholeTrack: '1000ms',
/** iOS Safari's indeterminate segment: one sweep of the track. */
sweep: '1000ms',
/** Under Reduce Motion, each direction of Safari's pulse instead. */
reducedMotionPulse: '1000ms',
/** The curve of all three: linear. */
easing: 'linear',
});

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-094 FR4 says every duration and curve a touch presentation plays MUST be a motion role named for its use, and a component MUST NOT play a literal. These four values are literals, and the progress table registers no role (motion/uikit/progressBar.ts:121-123); registering them as named roles of that table and reading them through the role path would conform, unless an engineering owner exempts them.

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

Approval is on hold until an engineering owner settles the governing spec for ProgressBar's indeterminate bar under a finger: no current record states Safari's 25% linear sweep, the 0.74 to 1.0 Reduce Motion pulse with no track, or whether spec:AST-094 FR4 needs motion roles for their timing. An engineering owner records that decision by commenting /approve-spec 2ea5d4697c0f4ec635fba15af6ca4709215e4931 on this pull request, and the author may do so as an engineering owner. Whatever that decision, please key the touch sweep on :dir(rtl) (ProgressBar.tsx:438), because a left-to-right bar inside a right-to-left page is empty for two thirds of each cycle, land #8225 and its parents first so this diff is its own 699 lines, and attach real iOS Safari and Android Chrome recordings.

[Automated review]

':is([dir="rtl"] *)': indeterminateSlideRtl,
'@media (pointer: coarse)': {
default: touchSweep,
':is([dir="rtl"] *)': touchSweepRtl,

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 key matches any right-to-left ancestor, while the 25% segment sits at the start of its own region, so a left-to-right bar inside a right-to-left page sweeps left off the track. On a 370 px track, 92.5, 57.8, 23.1 and 0 px are visible at 0, 125, 250 and 500 ms, and the bar is empty from 333 to 1,667 ms of each 2 s cycle. design:bidirectionality DR3 asks ordered progression to follow the containing region's direction. Keying the sweep on ':dir(rtl)', as the determinate fill does at lines 356 and 369, fixes it; a left-to-right-inside-right-to-left test like the one in ProgressBarMotion.a11y.chromium.spec.ts would keep it fixed.

Comment on lines +32 to +36
sweep: '1000ms',
/** Under Reduce Motion, each direction of Safari's pulse instead. */
reducedMotionPulse: '1000ms',
/** The curve of all three: linear. */
easing: 'linear',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These are literals played in place of motion roles. spec:AST-094 FR4 says every duration and curve a touch presentation plays MUST be a motion role named for its use and that a component MUST NOT play a literal, and registerProgressBarTable() registers progress with no roles. Unless an engineering owner rules otherwise on the open question, name the sweep, the pulse and the linear curve as roles in the generated table (DEC-5) and read them here.

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

Approval is on hold until an engineering owner settles the governing spec for ProgressBar's indeterminate bar under a finger: no current record states Safari's 25% linear sweep, the trackless 0.74 to 1.0 Reduce Motion pulse, or whether spec:AST-094 FR4 needs motion roles for their timing. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Whatever that decision, these changes are still needed:

  • Land #8225 and its parents first, or retarget onto #8225, so this diff is its own 699 lines rather than 18,847.
  • Key the touch sweep on :dir(rtl) (ProgressBar.tsx:438), because a left-to-right bar inside a right-to-left page is empty for two thirds of each cycle.
  • Attach real-device iOS Safari and Android Chrome recordings for the touch claims.

[Automated review]

':is([dir="rtl"] *)': indeterminateSlideRtl,
'@media (pointer: coarse)': {
default: touchSweep,
':is([dir="rtl"] *)': touchSweepRtl,

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 key matches any right-to-left ancestor, while the 25% segment sits at the start of its own region, so a left-to-right bar inside a right-to-left page sweeps left off the track: on a 370 px track 92.5, 57.8, 23.1 and 0 px are visible at 0, 125, 250 and 500 ms, and the bar is empty from 333 to 1,667 ms of each 2 s cycle. design:bidirectionality DR3 asks ordered progression to follow the containing region's direction. Keying the sweep on ':dir(rtl)', as the determinate fill does at lines 356 and 369, fixes it; a left-to-right-inside-right-to-left case like the one in ProgressBarMotion.a11y.chromium.spec.ts would keep it fixed.

Comment on lines +32 to +36
sweep: '1000ms',
/** Under Reduce Motion, each direction of Safari's pulse instead. */
reducedMotionPulse: '1000ms',
/** The curve of all three: linear. */
easing: 'linear',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These are literals played in place of motion roles. spec:AST-094 FR4 says every duration and curve a touch presentation plays MUST be a motion role named for its use and that a component MUST NOT play a literal, and registerProgressBarTable() registers progress with no roles. Unless an engineering owner rules otherwise on the open question, name the sweep, the pulse and the linear curve as roles in the generated table (DEC-5) and read them here.

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

Approval is on hold until an engineering owner settles the governing spec for ProgressBar's indeterminate bar under a finger: no current record states Safari's 25% linear sweep or the trackless 0.74 to 1.0 Reduce Motion pulse (spec:AST-094 leaves each component's touch presentation to its own record, and the ProgressBar record is still a draft), and the owners also need to say whether AST-094 FR4's motion roles apply to that timing. An engineering owner can record the decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), name the record that should state it, or keep today's slide on touch. Whatever that decision, these changes are needed:

  • Land #8225 and its parents first, or retarget onto #8225, so this diff is its own 702 lines rather than 17,109.
  • Key the touch sweep on :dir(rtl) instead of :is([dir="rtl"] *) (ProgressBar.tsx:438), because a left-to-right bar inside a right-to-left page is empty for two thirds of every cycle.
  • Attach recordings from a real iPhone in Safari and a real Android phone in Chrome for the touch claims, as AST-094's platform support requires.

[Automated review]

':is([dir="rtl"] *)': indeterminateSlideRtl,
'@media (pointer: coarse)': {
default: touchSweep,
':is([dir="rtl"] *)': touchSweepRtl,

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 key matches any right-to-left ancestor, while the 25% segment sits at the start of its own region, so a left-to-right bar inside a right-to-left page plays the right-to-left sweep from the left end and leaves the track: on a 370 px track 92.5, 57.8, 23.1 and 0 px of it are visible at 0, 125, 250 and 500 ms, and the track is empty from 333 to 1,667 ms of every 2 s cycle. design:bidirectionality DR3 asks ordered progression to follow the containing region's direction. Keying the sweep on ':dir(rtl)', as the determinate fill does at lines 356 and 369, fixes it; a left-to-right-inside-right-to-left case in ProgressBarTouch.a11y.chromium.spec.ts would keep it fixed.

Under a coarse primary pointer a determinate value change moves the way
UIProgressView's setProgress(_:animated: true) does: linearly, for the
progress.fillTrackMs role (1000 ms per whole track) times the share of the
track the value moved, on the progress.curves.fill role, scaled by the
theme's tempo, starting from wherever the fill is drawn. Above zero the
fill draws at least two track heights (16px), a mark that length covers
counts as on the fill, and at zero it paints nothing. A mouse keeps the
theme's timing, and reduced motion still lands at once
(component:ProgressBar FR4).
@vjeux
vjeux force-pushed the uikit/c07-progressbar-indeterminate branch from 9f35484 to c234eaf Compare November 2, 2026 07:03
@vjeux vjeux changed the title feat(ProgressBar): iOS Safari's indeterminate bar under a finger feat(ProgressBar): under reduced motion the indeterminate bar pulses in place Nov 2, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 2, 2026
…s at once

Under forced colors the fill is sized by width, a layout property that
touch motion never animates (spec:AST-094 FR5), so with a finger it now
lands at once there instead of taking the touch duration. The touch layer
no longer registers the progress table in a mount Effect: nothing at
runtime reads the registry, and the generated role table registers 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: the touch timing and the reduced-motion pulse match the ProgressBar record and their tests pass, but three small fixes are needed first.

  • With forced colors under a coarse pointer, the new timing drives the fill's width transition (800 ms, linear), which AST-094 FR5 rules out; land the sized fill at once there and add a forced-colors test.
  • Remove the dependency-free useEffect that calls registerProgressBarTable() in ProgressBarTouch.ts, since nothing reads the registry at runtime.
  • Scope the reduced-motion changeset's 3:1 promise to the built-in tokens: in the Neutral, Butter and Matcha themes, some tones fall below 3:1 at the 0.84 floor.

[Automated review]

default: durationVars['--duration-medium'],
// The touch layer's move (the `progress.fillTrackMs` role times the
// share moved), scaled by the theme's tempo (`spec:AST-108` FR3).
'@media (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.

Under forced colors this fill transitions width (line 333), and this coarse key has no forced-colors exception, so on a touch screen with a contrast theme a value change animates layout for 800 ms, linearly (10% → 90%), instead of landing. AST-094 FR5 says a touch presentation's animation MUST NOT animate layout properties. Nesting '@media (forced-colors: active)': '0s' under this key would land the sized fill at once there; a forced-colors case asserting no running width transition would keep it fixed.

const watchesTrack = isTouch && hasMarks;
const trackRef = useRef<HTMLDivElement | null>(null);
const [trackWidth, setTrackWidth] = useState(0);
useEffect(() => {

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 Effect names no external system and depends on nothing (react-component-runtime INV3), and nothing in ProgressBar reads the role registry at runtime: resolveMotion returns the value it is handed, and the role-table generator and roles tests register every table themselves. Deleting it (and the note in motion/uikit/progressBar.ts that the touch layer calls it) keeps behaviour the same; if a runtime entry is wanted, register at the read site as motion/coast.ts does.

'@astryxdesign/core': patch
---

[feat] Under reduced motion, an indeterminate ProgressBar no longer slides: on every pointer its fill spans the whole track and gently pulses its opacity in place, from full down to 84% and back, two seconds each way. A theme's tempo does not change the pulse, and every fill tone that stands out from the track at 3:1 still does at the pulse's lowest point.

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 3:1 promise holds for the built-in light and dark tokens (AR4 and the pulse-floor test), but not in every theme: at the 0.84 floor, Neutral light success goes from 3.39:1 to 2.79:1, Butter dark success from 3.12 to 2.64, Matcha light success from 3.11 to 2.56 and Matcha dark error from 3.23 to 2.66. Scoping it, for example to "in the built-in light and dark tokens", or dropping the clause, would keep the release note accurate.

vjeux added 2 commits November 2, 2026 00:39
…uch key

Core spells every pointer media key the way the touch presentation resolver
does, so the forced-colors 0s duration nests under the existing
(pointer: coarse) key instead of a compound key of its own.
…in place

Under reduced motion, on every pointer, the indeterminate fill spans the
whole track as one solid bar and only its opacity pulses, from 1 down to
0.84 and back, each way taking the progress.reducedMotionPulseMs role
(2000 ms) on the timing.curves.move curve; tempo does not scale it. The
0.84 floor keeps every tone that reaches 3:1 against the track at full
opacity at 3:1 in the light and dark default themes
(component:ProgressBar FR6, AR4).

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

Ready to approve: the touch timing and the reduced-motion pulse match the ProgressBar record, and the forced-colors fill, the extra Effect and the changeset wording from the last review are all fixed. The new Chromium specs and the unit tests pass. The inline notes are optional comment cleanups (a FillStyle comment that drifted onto PulseStyle, one long comment and one long file header).

[Automated review]

// inline style — which publishes the filled share as the private
// `--_progress-bar-fill` — needs this augmentation to typecheck.
type FillStyle = CSSProperties & Record<'--_progress-bar-fill', string>;
// The reduced-motion pulse's variables, from the progress table (FR6).

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: the three comment lines just above (517-519) explain FillStyle, but this new type now sits between them and type FillStyle. Move them down to sit directly above type FillStyle.

Comment on lines +313 to +318
// `--_progress-bar-flip` steps 0, 1, 2, 0… on every change and moves
// the fill by that many hundredths of a pixel, so a change back to
// the value it just left is never the running transition's own start
// value: CSS shortens a transition that reverses to its start, which
// would halve a return mid-flight (10 → 90 → 10). It takes three
// values because two would repeat on that return.

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: this states a real quirk, but the first sentence runs to about 55 words. A shorter form that keeps only the reason:

// CSS shortens a transition that reverses to its own start value, so a
// return mid-flight (10 → 90 → 10) would run at half length.
// `--_progress-bar-flip` steps 0, 1, 2 on each change and shifts the fill by
// that many hundredths of a pixel. Two steps would repeat on a return.

Comment on lines +5 to +25
/**
* @file ProgressBarTouch.ts
* @input Uses React (useEffect, useRef, useState), the touch presentation
* resolver (useTouchPresentation), the shared resize observer, and the
* progress table's roles (motion/uikit progressBar, read through
* resolveMotion)
* @output useProgressBarTouch: what the touch presentation needs from script,
* the length of the move a change makes (`--_progress-bar-move`: the
* `progress.fillTrackMs` role times the share of the track it moved), its
* curve (`--_progress-bar-curve`: the `progress.curves.fill` role), the
* three-step offset that keeps a return off CSS's reversing path
* (`--_progress-bar-flip`), and, under a finger with marks, the share of
* the track the fill draws at least (TOUCH_MIN_FILL_PX), measured on the
* track
* @position ProgressBar's touch layer, with one import site (ProgressBar.tsx):
* everything ProgressBar runs in script for a finger lives here
*
* SYNC: When modified, update /packages/core/src/ProgressBar/ProgressBar.tsx
* (its touch styles read these variables and the 16px minimum) and
* /packages/core/src/motion/uikit/progressBar.ts.
*/

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: this header runs to 21 lines, past the roughly 12-line limit for file headers. @output can name the hook and its three variables, with the detail left to the hook's own comments.

@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in e1e83a3, on top of #8225's fixes. With forced colors and a finger, the sized fill lands at once. The mount Effect is gone. The changeset's 3:1 promise now names the default light and dark themes. The whole core unit suite passes at that head.

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

Ready to approve: the touch fill timing and the reduced-motion pulse match the ProgressBar record, and with forced colors under a finger the sized fill still lands at once now that its rule sits under the touch key. The new Chromium specs and unit tests pass on this head. The inline notes are optional: a stale "two roles" count in the motion README and three comment trims. The repository's spec gate still needs an engineering owner (the author included, if they are one) to comment /approve-spec <full head SHA>, because the pull request touches two spec files.

[Automated review]

entry point. Core's motion and gesture modules read the table by path;
components read touch motion through its internal motion roles (`roles.ts`),
so each keeps its measured value; no shipped component reads the roles yet.
so each keeps its measured value; ProgressBar plays the progress table's two

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: since the pulse commit ProgressBar plays three of the progress table's roles (progress.reducedMotionPulseMs too), and the uikit/progressBar.ts row below lists only the fill's duration and curve. Say three roles here and add the reduced-motion pulse to that row.

// inline style — which publishes the filled share as the private
// `--_progress-bar-fill` — needs this augmentation to typecheck.
type FillStyle = CSSProperties & Record<'--_progress-bar-fill', string>;
// The reduced-motion pulse's variables, from the progress table (FR6).

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: the three comment lines just above explain FillStyle, but the new PulseStyle type now sits between them and type FillStyle. Move them down to sit directly above type FillStyle.

Comment on lines +313 to +318
// `--_progress-bar-flip` steps 0, 1, 2, 0… on every change and moves
// the fill by that many hundredths of a pixel, so a change back to
// the value it just left is never the running transition's own start
// value: CSS shortens a transition that reverses to its start, which
// would halve a return mid-flight (10 → 90 → 10). It takes three
// values because two would repeat on that return.

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: this states a real quirk, but the first sentence runs to about 55 words. A shorter form that keeps only the reason:

// CSS shortens a transition that reverses to its own start value, so a
// return mid-flight (10 → 90 → 10) would run at half length.
// `--_progress-bar-flip` steps 0, 1, 2 on each change and shifts the fill
// by that many hundredths of a pixel. Two steps would repeat on a return.

Comment on lines +5 to +25
/**
* @file ProgressBarTouch.ts
* @input Uses React (useEffect, useRef, useState), the touch presentation
* resolver (useTouchPresentation), the shared resize observer, and the
* progress table's roles (motion/uikit progressBar, read through
* resolveMotion)
* @output useProgressBarTouch: what the touch presentation needs from script,
* the length of the move a change makes (`--_progress-bar-move`: the
* `progress.fillTrackMs` role times the share of the track it moved), its
* curve (`--_progress-bar-curve`: the `progress.curves.fill` role), the
* three-step offset that keeps a return off CSS's reversing path
* (`--_progress-bar-flip`), and, under a finger with marks, the share of
* the track the fill draws at least (TOUCH_MIN_FILL_PX), measured on the
* track
* @position ProgressBar's touch layer, with one import site (ProgressBar.tsx):
* everything ProgressBar runs in script for a finger lives here
*
* SYNC: When modified, update /packages/core/src/ProgressBar/ProgressBar.tsx
* (its touch styles read these variables and the 16px minimum) and
* /packages/core/src/motion/uikit/progressBar.ts.
*/

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: this header runs to 21 lines, past the roughly 12-line limit for file headers. @output can name the hook and its three variables, with the detail left to the hook's own comments.

This branch was successfully deployed

1 active deployment
Preview — e1e83a39 Deployed Nov 2, 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 needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants