Skip to content

spec(AST-108): theme motion is three knobs — tempo, character, reducedMotion - #8613

Merged
cixzhang merged 27 commits into
mainfrom
spec/theme-motion-knobs
Oct 27, 2026
Merged

cixzhang merged 27 commits into
mainfrom
spec/theme-motion-knobs

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Decided (Cindy, 2026-10-26): a theme's motion is three knobs in defineTheme({motion}), generating the --duration-* and --ease-* tokens and the animator's base tables. Coordinated animations stay in sync, and springs need no CSS twin.

User impact

A theme author can make an app's motion faster or slower, crisper or livelier, or reduced for everyone, with one setting each, and CSS transitions and JavaScript springs follow together. Existing themes render exactly as today, because the defaults reproduce today's tokens and the measured springs.

Records

  • New spec:AST-108 theme motion:
    • FR1 {tempo, character, reducedMotion}. tempo is a number, default 1, clamped to 0.75–1.5. character has three steps, crisp, standard, or expressive (the adopted recommendation). reducedMotion is 'system' or 'always', with no loosening value.
    • FR2 generates the tokens. The defaults pin today's values: --duration-* 130, 175, 230, 310, 410, 550, 730, 975, 1300 ms and --ease-standard cubic-bezier(0.24, 1, 0.4, 1). It adds job-named --ease-entrance, --ease-move, and --ease-exit, and exits don't change with character.
    • FR3 base tables derive at the source: tempo scales duration, and bounce is the character amount times a fixed per-role weight. Defaults play the measured values exactly, springs keep their physics, and roles stay internal.
    • FR4a coordinated animations share one clock: sibling glide, presence, sheet and scrim, menu grow and content, and a CSS fade with a spring.
    • FR4 lists what's fixed on purpose.
    • FR5 is additive. MotionScaleConfig, expandMotionScale, and MotionScale* are unchanged. An old config maps onto tempo, explicit fields and tokens win, and extends keeps replacing.
    • FR6 covers the server: themes are plain data, 'system' is resolved by media-query CSS, and an internal pure resolveThemeMotion(theme) serves both. OQ1 asks whether to make it public.
    • FR7 lists the docs as done criteria.
    • DEC-1 records the decision.
  • spec:AST-094: new DEC-15 (the public motion set is three theme knobs; per-role tables stay internal; supersedes DEC-11 for exactly these knobs). FR4 now reads: tempo 1 plus standard plays every measured value exactly, and the fixed list is named.
  • design:reduced-motion: new DR2a. A theme may apply DR1–DR3 for everyone with 'always'; nothing loosens DR1–DR4.
  • design:motion (draft) DR3: an expressive character may overshoot on entrances and moves only.
  • spec:AST-090 (draft) FR1: a theme's tempo scales the exit tokens when they're set.

Implementation PRs follow this record, with tests that the CSS tokens and JS tables derive from each knob value and that paired animations end within one frame.

Writing rubric results

  • R1 present tense: pass. R2 research: none in the records.
  • R3 verification: rows for FR1–FR7 and FR4a.
  • R4 rejected alternatives: one line in each DEC.
  • R5 change references: none in the records. R6 open questions: AST-108 OQ1.

Extends behavior (FR5)

FR5 states shipped behavior: an extending theme inherits motion field by field. resolveThemeValues seeds the child's tokens from its base, and expandMotionScale writes only the fields the child sets, so an unset field keeps the base's tokens. The author-a-theme sentence "child config replaces base entirely" disagrees with that code and is being corrected separately.

…dMotion (AST-094 DEC-15, FR4; reduced-motion DR2a)
@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 6:14pm UTC

Request Review

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 76282bc

@astracat-bot

astracat-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 27, 2026, 6:44 PM UTC

@github-actions github-actions Bot added the spec-auto-merge Auto-merge was enabled by the spec owner gate label Oct 26, 2026
…heet's scrim follows the sheet's settle time
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 843655e

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Previews ready: Storybook and Sandbox were built with this PR's docsite. CI evidence will be added when available.

View Storybook for this PR · View Sandbox for this PR

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec ef37748

@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

A question on FR4's fixed list, which decides how #8268 is reshaped: which press-paint timers are "press clocks"?

The delegated press paint has three: the quick-tap flash (flashMs, how long a quick tap stays highlighted) and two release fades (buttonReleaseMs for a control, highlightOutMs for a cell), which pressable styles also play as CSS fades. #8268 resolves all three per theme, which DEC-15 rules out.

My read: the flash is a press clock and stays fixed. The release fades are motion, so tempo scales them, and FR4a then needs the consumer's fading timer to come from the same resolved value (resolveThemeMotion) rather than a second clock; #8268 would shrink to that. If the release fades are fixed too, #8268 closes.

@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Three questions on how FR3 and FR1's reducedMotion reach the runtime, facts first.

  1. In feat(core): the internal touch foundation: press tracking and the animator #8076 every spring and curve the animator plays, and every duration a component times, passes through resolveMotion(element, …), which today returns the role's table value and reads no style (spec:AST-094 DEC-11). That is where FR3's derivation would apply: a spring's duration times tempo, and its bounce offset by character times the role's weight. The tables carry no per-role weight yet. Should the weight live in each table beside the role's measured value, and does this spec record each role's weight, or the rule that sets it?
  2. FR6 keeps a theme plain data with no DOM. In the browser the animator still needs the knobs of the theme that applies to a given element. Should it read them from the element when a motion starts (custom properties the theme writes, the way --duration-* tokens reach CSS), or receive them as theme data through the component tree?
  3. reducedMotion: 'always' has two consumers. The animator's planner checks prefers-reduced-motion with matchMedia, and can read the knob too once it reaches the element. Components' CSS keys its Reduce Motion styles on media queries (@media (prefers-reduced-motion: reduce) and (pointer: coarse), the StyleX constants in motion/reducedMotion.stylex.ts), which cannot see a theme value. Should the theme generate the 'always' side, for example an attribute those styles also key on, or does 'always' cover only the animator?

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

Requesting changes: the three motion knobs are owner-approved direction, but a few statements in the records contradict each other or today's code. Please fix these before merging; the remaining notes are smaller wording fixes.

  • AST-108 FR2 lets character re-curve --ease-standard, which the Drawer, Toast, MobileNav and scrim exits read today, so “exits stay quiet” does not hold; keep --ease-standard fixed or move those exits to --ease-exit and give it a default.
  • FR5 maps every first-party theme's MotionScaleConfig to a tempo of 0.61–0.85, six of seven below the 0.75 clamp, while the record says existing themes render exactly as before; say whether the implied tempo is clamped and correct that line.
  • AST-094 FR1, DEC-1, DEC-3, its Current-state impact and its FR4 verification row still promise per-role theme overrides that the new DEC-15 rules out.
  • The new AST-090 FR1 sentence scales exit durations a theme sets explicitly, which conflicts with explicit tokens winning (theme-authoring-contract INV5, AST-108 FR5).

[Automated review]

Comment thread docs/specs/AST-108-theme-motion/spec.md Outdated
`--duration-*` tokens are their base values times `tempo`. `character` sets
the job-named `--ease-entrance` and `--ease-move` tokens, declared in
`tokens.stylex.ts`, and `--ease-standard`; `--ease-exit` is the same at
every character, so exits stay quiet. With `tempo` 1 and `character`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

--ease-standard is the curve today's Drawer, Toast, MobileNav and scrim exits read, so letting character re-curve it gives those exits the 'expressive' overshoot and makes “exits stay quiet” untrue. Consider keeping --ease-standard fixed at every character, or listing the exits that move to --ease-exit and giving --ease-exit its default here.

Comment thread docs/specs/AST-108-theme-motion/spec.md Outdated
`motion` also accepts the existing `MotionScaleConfig`, and
`expandMotionScale` and the `MotionScale*` types are unchanged. A
`MotionScaleConfig` keeps generating its tokens and gives the animator the
`tempo` its `medium` value implies against the default. Explicit

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Every first-party theme sets medium to 250–350 ms, so this implies a tempo of 0.61–0.85, and six of the seven fall below FR1's 0.75 clamp. Please say whether the implied value is clamped, and update “existing themes render exactly as before” and the FR5 verification row, since these themes' touch springs would change.


A theme reaches touch motion only through `spec:AST-108`'s three knobs, tempo,
character, and reduced motion, which transform the measured roles; the
per-role tables and weights stay internal. This supersedes DEC-11's "all

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR1 (“once the roles are public, a theme MAY override a motion role”), DEC-1, DEC-3, the Current-state impact line and the FR4 verification row still promise per-role overrides. Could DEC-15 say it supersedes DEC-1's and DEC-3's per-role retuning, with those lines reworded in terms of the knobs?

Comment thread docs/specs/AST-090-exit-motion/spec.md Outdated

- **FR1 — Exits are off by default.** `--duration-exit-dialog` and
`--duration-exit-layer` default to `0ms`. A surface whose kind's exit
`--duration-exit-layer` default to `0ms`. When a theme sets them, its tempo

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 theme turns exits on by writing these durations as tokens (FR12: “or to any other duration”), and explicit tokens win over tempo (AST-108 FR5, theme-authoring-contract INV5). Consider saying that a value referencing --duration-exit or another --duration-* token follows tempo through that reference, while a literal plays as written.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 8cbc8e7

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec e882416

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 74bd8ae

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…nter gestures) into the theme motion knobs

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

Requesting changes: the three motion knobs are owner-approved direction, but spec:AST-108 FR4 fixes only the press onset delay, cancel distance and tap flash, so the press release fade now follows tempo, while component:Button FR15 still promises “fading out over 200 ms on release”. Please amend Button FR15 to time the fade at tempo 1 and scale it with the theme's tempo (or add the fade to the fixed list in both spec:AST-108 FR4 and spec:AST-094 FR4 if it should stay fixed), and name Button FR15 in AST-108's Current-state impact, as was done for Spinner and Skeleton. Smaller fixes for the same pass:

  • spec:AST-094 FR4 should say the knobs join the motion configuration, since MotionScaleConfig stays.
  • spec:AST-094's FR4 verification row should fail a value off the table only at tempo 1 and 'standard'.
  • The Deciding specs section of architecture:theme-application should list spec:AST-108/DEC-1, and AST-108's amended-records line should name architecture:theme-authoring-contract.

[Automated review]

move
finger-following and drag motion, scroll momentum and decay, the rubber
band, the on-screen keyboard's motion (`spec:AST-099` FR8), the press
model's clocks (its onset delay, cancel distance, and tap flash), hover-intent

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 list fixes only the press model's onset delay, cancel distance and tap flash, so the press release fade follows tempo, but component:Button FR15 still says the pressed overlay is “fading out over 200 ms on release”. Could Button FR15 time the fade at tempo 1 and scale it with the theme's tempo, with Button named in Current-state impact as Spinner and Skeleton are? If the fade should stay fixed, it belongs on this list (and in spec:AST-094 FR4) instead.

| FR1, FR2 | Value check over every measured value and its uses | every value; a guess and its measurement task; a platform-specific value; two components playing the same motion | an unsourced value, an unknown source kind, a guess without a listed measurement, an unsourced platform split, or two values for one use fails |
| FR3 | Unit tests of the spring, decay, rubber band and projection math against an independent integration and values read from UIKit | under-, critically and overdamped springs; release velocities; both rates; overscroll depths | changing a formula or a constant breaks the agreement with UIKit's values |
| FR4 | Role unit tests over every touch motion | each role at its table value; a theme that writes `motion` | a literal played in place of a role, a value that differs from the table, or a theme that changes a role before the public set is decided fails |
| FR4 | Role unit tests over every touch motion | each role at its table value; a theme that writes `motion` | a literal played in place of a role, a value that differs from the table, or a theme that changes a role other than through its motion knobs fails |

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 theme that writes motion can now move a role's value through the knobs, so “a value that differs from the table” fails a correct tempo: 1.5 theme. Perhaps “a value that differs from the table at tempo 1 and 'standard'”?

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 6b94712

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

Requesting changes: the three motion knobs are owner-approved direction and the earlier contradictions are fixed, but two record details need a small edit before these records become current authority. In spec:AST-108 FR4a, “presence's enter and exit” names something no record defines and the FR4a verification row never tests, so please describe that pair by what a person sees (or drop it) and give the row a matching state. In architecture:theme-authoring-contract, resolveThemeMotion(theme) is listed under Owning code although it does not exist yet, so please record it under Known conformance and verification gaps as accepted but unshipped, as the record already does for INV8.

[Automated review]

Comment thread docs/specs/AST-108-theme-motion/spec.md Outdated
together start and land together: they play the same motion role, or the CSS
part is timed from the spring's computed settle time, and never as two
independently timed curves. This covers an element and the siblings that
glide around it (`spec:AST-105`'s one shared timing), `presence`'s enter and

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 record defines presence, and Core exports nothing by that name, so it is unclear which enter and exit pair this binds; the FR4a verification row also has no state for it. Could this name the pair by what a person sees, as the other four items do (or drop it), with a matching state in the FR4a row?

Comment on lines +252 to +255
- An internal, pure `resolveThemeMotion(theme)` returns a theme's motion
knobs, durations, easings, and role tables for the animator and for server
code; it touches no DOM and has no import-time side effects
(`spec:AST-108` 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.

resolveThemeMotion does not exist in Core yet, and unlike the other Owning code entries this one names no file. Could the record's Known conformance and verification gaps section say the resolver is accepted but unshipped, as it does for INV8, so readers do not take it for shipped code?

Comment thread docs/specs/AST-108-theme-motion/spec.md Outdated
- `tempo` is a number, default `1`, clamped to 0.75–1.5; it scales every
motion duration;
- `character` is `'crisp'`, `'standard'` (default), or `'expressive'`; it
picks the entrance and move curves and the spring bounce. a continuous bounce

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit, not blocking: “spring bounce. a continuous bounce” starts a sentence in lower case.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec b84e8f2

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

Recommending approval: the three motion knobs are owner-approved direction at this revision, and the records now agree with each other, with the current specs they leave unchanged, and with today's code (the pinned token defaults, field-by-field extends, the busy-loop tokens and Button's 200 ms release fade); the earlier presence and resolveThemeMotion notes are resolved. Before merging, please refresh the description's Records list, which still says FR4a covers presence, that extends keeps replacing, and that FR6 names the resolver. When the implementation lands, its FR6 tests should also cover portaled layers and separate React roots, as architecture:theme-application's change coupling asks of a root-attribute change.

[Automated review]

@cixzhang
cixzhang merged commit ab0db1f into main Oct 27, 2026
69 checks passed
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…nto the theme motion knobs

AST-108's record takes main's text, as merged. The theme authoring
contract keeps this branch's additions (INV15, the motion files it owns,
AST-108 decision 1) and lists resolveThemeMotion under owning code,
which this branch ships: its known gap for the unshipped resolver goes.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
Button.spec.md FR15: main's text (meta#8613: the 200 ms release at tempo 1,
scaled by the theme's tempo, spec:AST-108 FR4) with meta#8118's own
additions kept; the touch release's measured 250 ms is likewise stated
at tempo 1.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
… paint

Button.spec.md FR15: main's text (meta#8613: the 200 ms release at tempo 1,
scaled by the theme's tempo, spec:AST-108 FR4) with this pull request's
own wording kept; the measured 250 ms release is likewise stated at
tempo 1.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
… through c02-8124-base (main's meta#8613 Button.spec.md conflict)
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…0718, meta#8124's P14 check) into uikit/c02-switch-every-pointer (main's meta#8613 Button.spec.md conflict)
@github-actions
github-actions Bot deleted the spec/theme-motion-knobs branch October 28, 2026 06:17
cixzhang pushed a commit that referenced this pull request Oct 31, 2026
## Why

A theme author cannot say how motion should feel, quicker, crisper or reduced, without overriding durations and curves per component. `spec:AST-108` (#8613) gives `defineTheme`'s `motion` three knobs: `tempo`, `character` and `reducedMotion`. This pull request ships `tempo` and `character`, and exports their types (`ThemeMotionConfig`, `ThemeMotionKnobs`, `MotionCharacter`). `reducedMotion` stays out of the public type until `'always'` works: component Reduce Motion styles, script readers and the animator must follow it first. Every theme follows each person's setting, as `'system'` does.

## What led to it

This comes from the work to make Astryx's touch components behave as native iOS ones do, where a theme's motion must reach springs and press feedback, not only CSS. Themes could scale their duration tokens, but easing was one curve for everything, and Reduce Motion only followed the system setting.

## Options and tradeoffs

- CSS-only knobs compose through the cascade but cost the animator a style read per motion; attribute-only knobs are free for scripts but invisible to CSS.
- Busy loops (Spinner, Skeleton) could be exempted from `tempo` by name, or read a copy of each duration taken before `tempo`.
- An extending theme could resolve base first, then generated (the theme-authoring contract), or let a base's explicit motion tokens win over its children's knobs.

## Why this choice

- CSS gets tokens now; the attributes the animator reads come with the animator's own pull request.
- The copies: every layer that writes a duration writes one, so later components need nothing special.
- The contract's precedence, as the maintainer ruled; a child with no `slow` keeps its base's slow band (#8748).
- `--ease-exit` is #8004's; this defines none, and `character` never moves it.
- `reducedMotion: 'always'` would have reached only motion-keyed adaptation rules and the press release fade, so a theme setting it would reduce a little of the page and leave Spinner, Skeleton and most components moving. With `'system'` as the only value the field would do nothing, so the public type leaves it out until every component follows `'always'`.

## How to check it

1. Wrap a Dialog story in `<Theme theme={defineTheme({name: 'Slow', motion: {tempo: 1.5}})}>`: it opens 1.5 times slower; a Spinner beside it keeps its speed.
2. Add `character: 'expressive'`: `--ease-entrance` and `--ease-move` take the overshooting curve, so a style that eases on them overshoots on entrance; `--ease-standard` and exits do not change. Dialog still eases on `--ease-standard`, so it does not overshoot yet.
3. Under tempo 1.5, tap or click a Button: its pressed highlight fades out over 300 ms instead of 200 ms.
4. Extend a theme that sets `--duration-medium: 300ms` with `tempo: 1.2`: the child's is 492 ms.
5. `defineTheme({name: 'Partial', motion: {fast: 100, tempo: 1.2}})` throws: a duration scale needs `fast`, `medium` and `ratio`, from the theme or its base.

## Evidence

- Tests: `packages/core/src/theme/themeMotion.test.ts` and `packages/core/src/utils/pressFeedback.test.ts`. Each new case fails without its part of the change.
- The theme, press feedback, Spinner and Skeleton suites, the theme private-variable and CSS build checks, the CLI doc goldens, type check and lint pass on this head.

## Writing rubric results

R1 narration, R2 research, R4 rejected alternatives, R5 change references and R6 open questions: none. R3 evidence: INV15's row cites its test file.

## Scope

- [x] One capability is added.
- [x] Unrelated fixes, cleanup, and policy changes were removed or split.
- [x] Consumer docs, focused tests, and migration evidence ship with the feature.
- [x] Public text and artifacts contain no internal Meta context.

This branch was successfully deployed

1 active deployment
Preview — b84e8f2a 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants