Skip to content

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/motion

import { Stagger } from '@youj/motion'

For 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: itemClassName goes on each wrapper, for example itemClassName="h-full" so the cards fill their cells and match heights.
  • A different class per item: wrap that child in a Reveal yourself and give the Reveal the class. A Reveal child 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.

Reference

Propsas, itemAs, itemClassName… (13)

Props

Stagger props
PropType
asElementType

default 'div'

'div'
itemAsRevealTag
'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'
itemClassNamestring–
variantRevealVariant
'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'
stepStaggerToken
'tight''base''loose'

default 'base'

'base'
orderStaggerOrder
'start''end''center''random'

default 'start'

'start'
seednumber

Seed for 'random' order.

default 1

1
delaynumber

Initial delay, in steps.

default 0

0
inViewInView

When it plays: false on mount; true (the default) when scrolled into view, once, at 20% visible; or { once, amount, margin }.

–
childrenReactNode–
Other props pass through: extends HTMLAttributes<HTMLElement>.
MotionCue offset, dense lists, hero sections… · reduced motion

Motion

Motion tokens
MomentTokens
Cue offsetbetween neighboring ranks, default step
stagger.base

40ms step

40ms step
Dense listsstep="tight"
stagger.tight

24ms step

24ms step
Hero sectionsstep="loose"
stagger.loose

72ms step

72ms step
Each childthe default rise variant
duration.slowease.enter

340ms · 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 as and itemAs to 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.