Stagger
Animate a group of elements in sequence, with shared timing and order controls.
Use it when a group arrives together: a grid of cards, a feature list, a row of stats. The offsets are tens of milliseconds, so the group reads as one gesture, not a queue. Don't stagger content people are waiting to read, like search results or a table that has just loaded. Show those at once.
Installation
npm i @youj/motionFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Usage
Every child gets its own cue. A child that isn't a Reveal is wrapped in one, using the group's variant.
import { Stagger } from '@youj/motion'
export function Features() {
return (
<Stagger className="grid grid-cols-3 gap-4">
<FeatureCard title="Previews" />
<FeatureCard title="Rollbacks" />
<FeatureCard title="Analytics" />
</Stagger>
)
}The wrapper is the grid item. Each child sits inside its own div, so a layout class on the child (col-span-2, justify-self-end) no longer reaches the grid: the wrapper is what the grid lays out. Two ways to put it where it counts:
- The same class for every item:
itemClassNamegoes on each wrapper, for exampleitemClassName="h-full"so the cards fill their cells and match heights. - A different class per item: wrap that child in a
Revealyourself and give theRevealthe class. ARevealchild isn't wrapped again, so it is the grid item.
<Stagger className="grid grid-cols-3 gap-4" itemClassName="h-full">
<Reveal className="col-span-2">
<FeatureCard title="Previews" />
</Reveal>
<FeatureCard title="Rollbacks" />
<FeatureCard title="Analytics" />
</Stagger>itemClassName goes on the wrappers Stagger makes, not on a Reveal you wrote. Give your own Reveal the classes it needs, h-full included.
A Reveal child keeps its own variant, and its own delay adds to the cue the group gives it. Mix them when one item should arrive differently:
<Stagger step="loose">
<Reveal as="h2" variant="blur-in">Pricing</Reveal>
<PlanCard plan="hobby" />
<PlanCard plan="pro" />
</Stagger>Examples
Order
order decides which child moves first. start reads top to bottom, end runs in reverse, and center ripples out from the middle of the sequence. random shuffles the cues with a seed, so the server and the client agree on the order and the page hydrates without a jump.
Ranks follow the children's order in the source, not their position on screen. In a grid, center starts from the middle item and works outward in both directions.
Lists
Pass as="ul" and itemAs="li" to keep list semantics. Each child is wrapped in an animated li, so screen readers still announce a list and its length. A tight step with fade suits dense rows.
When it plays
With the default inView, the group plays once, when 20% of it is on screen. Pass an object to change that: inView={{ amount: 0.5 }}, margin to start earlier or later, and once: false to replay every time it comes back into view. inView={false} plays as soon as the group renders. Use it for content that is already on screen when the page opens.
delay holds the whole sequence back by a number of steps, so it stays on the same grid as the cues. delay={3} with step="base" waits three base steps before the first child moves.
Reference
Propsas, itemAs, itemClassName… (13)
Props
| Prop | Type | |
|---|---|---|
as | ElementTypedefault 'div' | 'div' |
itemAs | RevealTag'div''span''section''article''header''footer''li''p''h1''h2''h3''figure'Tag for the Reveal wrapper around children that aren't already a Reveal. default 'div' | 'div' |
itemClassName | string | – |
variant | RevealVariant'fade''rise''blur-in''scale''wipe-up''wipe-down''wipe-left''wipe-right'Variant for wrapped children. Reveal children keep their own. default 'rise' | 'rise' |
step | StaggerToken'tight''base''loose'default 'base' | 'base' |
order | StaggerOrder'start''end''center''random'default 'start' | 'start' |
seed | numberSeed for 'random' order. default 1 | 1 |
delay | numberInitial delay, in default 0 | 0 |
inView | InViewWhen it plays: | – |
children | ReactNode | – |
Other props pass through: extends HTMLAttributes<HTMLElement>. | ||
MotionCue offset, dense lists, hero sections… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Cue offsetbetween neighboring ranks, default step | stagger.base40ms step | 40ms step | |
Dense listsstep="tight" | stagger.tight24ms step | 24ms step | |
Hero sectionsstep="loose" | stagger.loose72ms step | 72ms step | |
Each childthe default rise variant | duration.slowease.enter340ms · cubic-bezier(0.16, 1, 0.3, 1) | 340mscubic-bezier(0.16, 1, 0.3, 1) |
Reduced motion
Every child renders in its final state at once. There's no sequence and no travel.
Each child animates with its Reveal variant, so the timing of one item is the same as a lone Reveal. Only the start times differ.
Reduced motion
The group appears complete, with no cascade. Nothing is hidden or delayed, so the page reads the same with or without motion. Switch on Reduced motion in any preview above to compare.
AccessibilityARIA
Accessibility
- Every child is in the document from the first render, in source order, so assistive technology reads the whole group whether or not it has played.
- Stagger adds no roles. Use
asanditemAsto give the group and its items their semantics. - The sequence plays once by default. Content that re-animates each time it scrolls past distracts, and it's hard to read for people with attention or vestibular conditions.