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
-
Motion tokens.
Theme.motiongains 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-staggerand--ease-stagger, and sets every duration to0msunderprefers-reduced-motion: reduce, as for the other steps. -
motionTiming.@stackra/tokensexportsmotionTiming(theme, name), which reads a step as milliseconds and a cubic bezier for animation libraries in JavaScript (Motion, Reanimated, Recharts, NumberFlow). It throws aRangeErrorfor a missing step or a timing it cannot read. Asteps()easing returns no bezier, and the caller uses its own curve. Components read timings through it, never as literals. -
Reveal.@stackra/uishipsReveal, an entrance that fades its child in and raises it 8 pixels on therevealstep, delayed byindex×stagger. On the web it is a Motionmotion.div. On native it is a ReanimatedAnimated.Viewwith aFadeInDownentering animation.isAnimated={false}renders the child in place. Under reduced motion, the web version starts at rest (Motion’suseReducedMotion), and the native one follows the system setting (ReduceMotion.System). -
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.Chartdraws its sparkline in on therevealstep. Recharts’isAnimationActive: "auto"keeps it still under reduced motion and on the server. A caller’sareaPropswin. - Native charts (
AreaChart,BarChart,LineChart,PieChart,ComposedChart) give their series a timinganimateon therevealstep, so a series moves to new data instead of jumping. Reanimated’s timing follows the system’s reduce-motion setting. A caller’sanimatewins, and the root’sanimation="disable-all"still stills every series. Grouped bars (BarGroupItem) keep HeroUI’s default, becauseBarGroupreads its items’ props without rendering them. - The web
NumberValuerolls its number with NumberFlow on therevealstep (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 asar-SAandar-EGwith their Arabic-Indic digits. NumberFlow reads each digit as a Latin one.
-
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. -
Unchanged, because the library has no animation. The native
NumberValueandTrendChipon both platforms draw still. HeroUI Native Pro offers no animation to turn on, and Stackra does not add one. -
Glass. Motion never animates the opacity of a native
GlassViewor 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.Listtherefore reveals its rows inside itsSurfaceorListGroup, not around it.@stackra/ui-pronever 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
revealstep in the theme retimes every entrance, chart and number together. @stackra/uitakes Motion as a peer of its./reactentry, optional like the entry’s other UI libraries, so a web app that imports@stackra/ui/reactinstalls 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/tokenstests:motionTimingand the reduced-motion block of the web stylesheet.@stackra/uitests:Revealon both platforms andList’s reveal.@stackra/ui-protests:Kpi.Chart, the native chart series andNumberValue.- 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
opacityon a nativeGlassViewor its ancestors (decision 7); until it lands, design review checks it.