Repository navigation
spec(AST-108): theme motion is three knobs — tempo, character, reducedMotion - #8613
Conversation
…dMotion (AST-094 DEC-15, FR4; reduced-motion DR2a)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
/approve-spec 76282bc |
|
AI review status for this pull request.
|
…heet's scrim follows the sheet's settle time
|
/approve-spec 843655e |
PR Analysis Report
|
|
/approve-spec ef37748 |
|
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 ( My read: the flash is a press clock and stays fixed. The release fades are motion, so |
|
Three questions on how FR3 and FR1's
|
There was a problem hiding this comment.
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
characterre-curve--ease-standard, which the Drawer, Toast, MobileNav and scrim exits read today, so “exits stay quiet” does not hold; keep--ease-standardfixed or move those exits to--ease-exitand give it a default. - FR5 maps every first-party theme's
MotionScaleConfigto 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]
| `--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` |
There was a problem hiding this comment.
--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.
| `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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
|
|
||
| - **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 |
There was a problem hiding this comment.
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.
…tempo unclamped; per-role override promises retired; explicit exit tokens win
|
/approve-spec 8cbc8e7 |
…ns only; springs stay at tempo 1
|
/approve-spec e882416 |
|
/approve-spec 74bd8ae |
…nter gestures) into the theme motion knobs
There was a problem hiding this comment.
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-094FR4 should say the knobs join the motion configuration, sinceMotionScaleConfigstays.spec:AST-094's FR4 verification row should fail a value off the table only attempo1 and'standard'.- The Deciding specs section of
architecture:theme-applicationshould listspec:AST-108/DEC-1, and AST-108's amended-records line should namearchitecture: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 |
There was a problem hiding this comment.
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 | |
There was a problem hiding this comment.
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'”?
|
/approve-spec 6b94712 |
There was a problem hiding this comment.
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]
| 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 |
There was a problem hiding this comment.
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?
| - 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). |
There was a problem hiding this comment.
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?
| - `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 |
There was a problem hiding this comment.
Nit, not blocking: “spring bounce. a continuous bounce” starts a sentence in lower case.
|
/approve-spec b84e8f2 |
There was a problem hiding this comment.
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]
…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.
…or's theme springs
…rt's settle clock
… 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.
## 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.
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
spec:AST-108theme motion:{tempo, character, reducedMotion}.tempois a number, default 1, clamped to 0.75–1.5.characterhas three steps, crisp, standard, or expressive (the adopted recommendation).reducedMotionis'system'or'always', with no loosening value.--duration-*130, 175, 230, 310, 410, 550, 730, 975, 1300 ms and--ease-standardcubic-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.MotionScaleConfig,expandMotionScale, andMotionScale*are unchanged. An old config maps onto tempo, explicit fields and tokens win, andextendskeeps replacing.'system'is resolved by media-query CSS, and an internal pureresolveThemeMotion(theme)serves both. OQ1 asks whether to make it public.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
Extends behavior (FR5)
FR5 states shipped behavior: an extending theme inherits motion field by field.
resolveThemeValuesseeds the child's tokens from its base, andexpandMotionScalewrites 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.