Stagger
betaShows its children fading up one after another as they first appear, for lists, card grids, and sections on page load.
Preview
Each group fades up once, as the page loads. Reload the page to see it again.
grid
list
- Mon 09:30 · Planning, Room 1
- Mon 11:00 · Design review, Studio
- Tue 13:00 · Team sync, Room 2
- Wed 10:00 · Workshop, Room 3
- Thu 14:00 · Demo day, Boardroom
12 items
- Item 1
- Item 2
- Item 3
- Item 4
- Item 5
- Item 6
- Item 7
- Item 8
- Item 9
- Item 10
- Item 11
- Item 12
slower
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/stagger
Use it for
- A list, a grid of cards, or the sections of a page that arrive together, so the eye reads them in order.
- Wrap the items once: <Stagger className="grid gap-4">{cards}</Stagger>. Each child becomes one item.
- Put the layout classes on Stagger. Each child is wrapped in a <div>, so the wrappers are the grid or flex items.
- Pass as="ul" or as="ol" for a list. Each child is then wrapped in an <li>, so pass the content, not <li> elements.
- Only the first eight items step, 35ms apart. The rest arrive with the eighth, so a long list is whole in under half a second.
Avoid it for
- Menus, selects, and other popups. Their items appear at once, in one piece.
- Content that updates often, like a live feed. Only new items fade in, and the wait between them reads as lag.
- Wrapping a single element. There is nothing to put in order.
- Nesting Staggers. The inner items wait on top of the outer wait.
Props
- children
The items. Each child fades up in turn, wrapped in a <div>, or an <li> in a list.
ReactNode
- step
Milliseconds between items in the default feel. Calm and Playful stretch it with their durations, and motion off makes it 0.
number · default 35
- delay
Milliseconds before the first item, scaled the same way as step.
number · default 0
- as
The element that holds the items. With ul or ol, each item is an <li>.
"div" | "section" | "ul" | "ol" · default "div"
- className
Classes on the outer element, like a grid or a gap.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| children | ReactNode | - | The items. Each child fades up in turn, wrapped in a <div>, or an <li> in a list. |
| step | number | 35 | Milliseconds between items in the default feel. Calm and Playful stretch it with their durations, and motion off makes it 0. |
| delay | number | 0 | Milliseconds before the first item, scaled the same way as step. |
| as | "div" | "section" | "ul" | "ol" | "div" | The element that holds the items. With ul or ol, each item is an <li>. |
| className | string | - | Classes on the outer element, like a grid or a gap. |
Accessibility
- Nothing is hidden from screen readers while it fades. Every item is in the page from the first render.
- Reduced motion takes out the rise, so items only fade in place. With motion off, every item shows at once.
- It uses CSS @starting-style, so it needs no script. Browsers without it show every item at once.
- With as="ul" or as="ol", it is a real list, so screen readers say how many items there are.
Examples
A grid of cards (needs @txbrendan/card)
import { Card } from "@/components/ui/card";
<Stagger className="grid gap-4 sm:grid-cols-3">
{teams.map((team) => (
<Card key={team.id} className="h-full px-4">
{team.name}
</Card>
))}
</Stagger>A list
<Stagger as="ul" className="flex flex-col gap-2">
{members.map((member) => (
<span key={member.id}>{member.name}</span>
))}
</Stagger>Slower, after the page title
<Stagger step={60} delay={120} className="flex flex-col gap-6">
<section>…</section>
<section>…</section>
</Stagger>