Motion
Two components — Appear for one thing, AppearGroup for siblings that should cascade — plus one
switch for turning animation off at any scope.
Tokens
Every duration and curve in the system, in one place. They are the same in all eight libraries —
only the type differs (a CSS custom property, a number, an Easing).
| Token | Value | Used for |
|---|---|---|
| base | 500ms | The default. Anything entering or leaving. |
| fast | 300ms | A state change on something already on screen. |
| stagger | 70ms | The gap between siblings in a cascade. |
| pulse | 1200ms | One breath of a live indicator. |
| dialSweep | 800ms | A dial arc drawing itself. |
| reelDigitStagger | 55ms | The gap between digits in a reel, left to right. |
| easing.out | cubic-bezier(0.16, 1, 0.3, 1) | Ease-out exponential. The system default. |
| easing.reel | cubic-bezier(0.25, 1, 0.5, 1) | Ease-out quart. Settles a long way into place. |
Appear
HTML / CSS · .hg-ai-f / .hg-ai-fd
An entrance. fade is opacity alone; rise also lifts 10px — the
ai-f and ai-fd ladder from the design.
Entrance only. It runs once, on the way in. It does not re-run when a parent re-renders or a list re-sorts, because a flourish that replays on every state change reads as a bug rather than a flourish.
Preview
Fades in and rises
Props
| Prop | Type | Notes |
|---|---|---|
| variant | "rise" | Omit for a plain fade. |
| delay | number | ms. Overrides any stagger inherited from a group. |
| duration | number | ms. |
| animate | "false" | Renders the final state with no animation. |
| — | The element animates itself and leaves your markup alone — a wrapper would break the nth-child the stagger depends on. |
Usage — HTML / CSS
<!-- The classes are the component. hg-ai-f fades; hg-ai-fd fades and rises. -->
<div class="hg-card hg-ai-fd">…</div>
<!-- An explicit delay, for one item that should wait -->
<div class="hg-card hg-ai-f" style="--hg-delay: 200ms">…</div>
AppearGroup
HTML / CSS · .hg-stagger
Stagger is the gap between siblings entering. Each item's animation starts a fixed interval after the one before it, so the group reads as a cascade rather than one block flashing in.
The delay comes from each child's position, not from a key. So re-ordering a list does not re-animate it, and an item appended later inherits the last delay rather than starting a fresh cascade.
maxStaggerMs caps the accumulated delay. Without a cap the hundredth row of a list
waits seven seconds before appearing; past the cap every remaining child starts together, which
is fine, because by then the cascade has been read.
On the web none of this costs any JavaScript: the index comes from nth-child and the
cap from min(), both in the stylesheet, so a staggered list survives server
rendering and needs no measurement.
Preview
Props
| Prop | Type | Notes |
|---|---|---|
| stagger | number | ms between one child starting and the next. Defaults to 70. |
| max-stagger | number | ms ceiling on the accumulated delay. Defaults to 500. |
| animate | "false" | Switches the whole group off. |
| — | Children must be <hg-appear> elements, or carry class="hg-ai-f". |
Usage — HTML / CSS
<!--
The group sets two custom properties; nth-child supplies each child's index
and min() applies the cap. No JavaScript is involved.
-->
<div class="hg-stagger" style="--hg-stagger-step: 70ms; --hg-stagger-max: 500ms">
<div class="hg-row hg-ai-fd">…</div>
<div class="hg-row hg-ai-fd">…</div>
<div class="hg-row hg-ai-fd">…</div>
</div>
Switching motion off
Every animated component can be stilled, at three scopes — the whole app, one subtree, or a single component. An app that wants to offer its users a "reduce motion" setting of its own needs the first; the other two exist because sometimes only one screen is the problem.
| Scope | Web | React Native |
|---|---|---|
| Whole app | data-motion="off" on <html> | <MotionProvider enabled={false}> |
| A subtree | data-motion="off" on any element | <MotionProvider> anywhere |
| One component | animate={false} | animate={false} |
On the web all three are the same CSS rule — animate={false}
renders nothing more than data-motion="off" onto that element. So there is one
mechanism to learn and one place it can go wrong. React Native has no CSS, so there the provider
and the prop are combined inside each component instead.
Preview
Usage — HTML / CSS
<!-- The whole app -->
<html data-motion="off">
<!-- One screen, or one panel -->
<section data-motion="off">…</section>
<!-- One component -->
<button class="hg-btn hg-btn--primary" data-motion="off">Save</button>
Reduced motion
The operating system's prefers-reduced-motion is honoured in every library,
whatever the application asked for. It is a separate path from the switch above,
and deliberately so: the switch is your setting, this is the user's, and yours cannot override
theirs.
The effect is the final state with no travel — never a frozen or missing element. Everything still appears; it simply appears rather than arrives. The one thing that is not suppressed is a value changing: a reel still shows the new number, it just does not roll to it.
The switch can only take motion away, never add it back — nesting a
MotionProvider enabled inside a disabled one does not re-enable anything, and no
prop overrides the OS preference.
Next
Components — all 23, with props, types and code samples.