> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stackra.figentra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 0329 — Motion layer: motion tokens, Reveal, and animation on by default

> Accepted · 2026-10-24

## Context

The design system had motion tokens for a few steps (`--duration-*` and `--ease-*` in `@stackra/tokens`), but no shared
entrance, and each component kept its library's animation default. Some of those defaults are off. HeroUI Pro turns the
KPI sparkline's animation off. HeroUI Native Pro's chart series take an `animate` config that is unset by default, so a
series jumps when its data changes. HeroUI Pro's and HeroUI Native Pro's `NumberValue` swap the number without
animating, although our docs said it animated. Lists had no entrance. An app that wants motion then sets it per
screen, with its own timings, and forgets reduced motion in some of those places.

Liquid Glass adds limits. Animating the opacity of a native `GlassView`, or of a view that contains one, breaks the
material. Animating a blur radius on the web repaints the whole backdrop on each frame.

## Decision

1. **Motion tokens.** `Theme.motion` gains two steps. `reveal` (400ms, `cubic-bezier(0.22, 1, 0.36, 1)`) is an
   entrance: a row arriving, a chart drawing in, a number rolling. `stagger` (40ms) is the delay between items that
   reveal in a row; only its duration is read. The web stylesheet renders them as `--duration-reveal`, `--ease-reveal`,
   `--duration-stagger` and `--ease-stagger`, and sets every duration to `0ms` under `prefers-reduced-motion: reduce`,
   as for the other steps.

2. **`motionTiming`.** `@stackra/tokens` exports `motionTiming(theme, name)`, which reads a step as milliseconds and a
   cubic bezier for animation libraries in JavaScript (Motion, Reanimated, Recharts, NumberFlow). It throws a
   `RangeError` for a missing step or a timing it cannot read. A `steps()` easing returns no bezier, and the caller
   uses its own curve. Components read timings through it, never as literals.

3. **`Reveal`.** `@stackra/ui` ships `Reveal`, an entrance that fades its child in and raises it 8 pixels on the
   `reveal` step, delayed by `index` × `stagger`. On the web it is a Motion `motion.div`. On native it is a Reanimated
   `Animated.View` with a `FadeInDown` entering animation. `isAnimated={false}` renders the child in place. Under
   reduced motion, the web version starts at rest (Motion's `useReducedMotion`), and the native one follows the
   system setting (`ReduceMotion.System`).

4. **Animation on by default.** Each of these animates unless its caller turns it off. Each respects reduced motion.
   * `List`, on both platforms, reveals its rows as one block inside its surface (`isAnimated`, on by default). A
     per-row stagger is left out: rows arrive in pages and recycle, so one entrance for the block keeps a reused row
     from replaying.
   * The web `Kpi.Chart` draws its sparkline in on the `reveal` step. Recharts' `isAnimationActive: "auto"` keeps it
     still under reduced motion and on the server. A caller's `areaProps` win.
   * Native charts (`AreaChart`, `BarChart`, `LineChart`, `PieChart`, `ComposedChart`) give their series a timing
     `animate` on the `reveal` step, so a series moves to new data instead of jumping. Reanimated's timing follows the
     system's reduce-motion setting. A caller's `animate` wins, and the root's `animation="disable-all"` still stills
     every series. Grouped bars (`BarGroupItem`) keep HeroUI's default, because `BarGroup` reads its items' props
     without rendering them.
   * The web `NumberValue` rolls its number with NumberFlow on the `reveal` step (`isAnimated`, on by default).
     NumberFlow stays still under reduced motion. A render-prop child gets HeroUI Pro's still number, and so does
     anything NumberFlow cannot draw: the scientific and engineering notations, and a locale whose digits are not
     Latin, such as `ar-SA` and `ar-EG` with their Arabic-Indic digits. NumberFlow reads each digit as a Latin one.

5. **Unchanged, because they already comply.** Web charts keep HeroUI Pro's Recharts defaults: they animate, with
   `isAnimationActive: "auto"`. Progress (bar, circle, meter) and toasts animate on both platforms and respect reduced
   motion. They keep their libraries' timings. `LineChart.AnimatedLine`, the native draw-on line, is HeroUI Native Pro's
   own.

6. **Unchanged, because the library has no animation.** The native `NumberValue` and `TrendChip` on both platforms
   draw still. HeroUI Native Pro offers no animation to turn on, and Stackra does not add one.

7. **Glass.** Motion never animates the opacity of a native `GlassView` or of any view that contains one, and never
   animates a blur radius on the web. An animation moves the transform or the opacity of the element inside the
   material. `List` therefore reveals its rows inside its `Surface` or `ListGroup`, not around it. `@stackra/ui-pro`
   never imports HeroUI Pro's glass theme, and its surfaces stay opaque. Refraction is deferred.

## Consequences

* A screen gets motion from its components, with one set of timings, and reduced motion holds without per-screen code.
* Changing the `reveal` step in the theme retimes every entrance, chart and number together.
* `@stackra/ui` takes Motion as a peer of its `./react` entry, optional like the entry's other UI libraries, so a web
  app that imports `@stackra/ui/react` installs it, and a native app does not.
* An end-to-end accessibility scan can run while content is still fading in, and read a colour mid-fade. A scan
  therefore emulates reduced motion, which puts every entrance at rest before it is checked.

## Enforcement

* `@stackra/tokens` tests: `motionTiming` and the reduced-motion block of the web stylesheet.
* `@stackra/ui` tests: `Reveal` on both platforms and `List`'s reveal.
* `@stackra/ui-pro` tests: `Kpi.Chart`, the native chart series and `NumberValue`.
* The playground's end-to-end axe scans emulate reduced motion (`apps/playground/e2e/__tests__/playground.e2e.test.ts`).
* planned — an oxlint rule that refuses an animated `opacity` on a native `GlassView` or its ancestors (decision 7);
  until it lands, design review checks it.

## Supersedes and sources

Supersedes: none. Sources: STA-430 and the owner's glass decision of 2026-10-24, restated in this ADR's own words.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.