unscripted/ui

Stagger

Staggered entrance animations where each item computes its own delay from sibling-index(). The cascade you'd reach for a motion library to get, in two CSS declarations per item.

sibling-indexexit animations

How it works

  • sibling-index() computes the delay: motion-safe:[transition-delay:calc(sibling-index()*100ms)] gives each item a delay from its 1-based position among its siblings. No per-item delay classes, no loops, no timers — add or reorder items and the cascade just follows.
  • The entrance itself is @starting-style: base classes are the settled state, starting:opacity-0 starting:translate-y-2 is the frame each item transitions from the first time it renders. transition-[opacity,translate] duration-500 ease-out does the rest.
  • The demo’s toggle replays the cascade without moving the layout: peer-checked/replay:*:translate-y-2 peer-checked/replay:*:opacity-0 on the list moves every item to its hidden frame, so checking staggers them out and unchecking staggers them back in. Opacity and translate don’t affect layout, so nothing below the list shifts.
  • @starting-style also applies when an element comes back from display: none, so the entrance runs whenever your framework mounts the fragment — not only on full page load.
  • The delay is gated behind motion-safe: because the global reduced-motion rule shortens durations but leaves delays alone, and a delayed item holds its invisible starting frame until the delay expires. This is the one motion pattern in the library where the global rule isn’t enough by itself.
  • The landing page of this site cascades its header and showcase cards with the same sibling-index() delay trick.

Accessibility

  • The motion is decorative. Every item is in the DOM from the first byte, so crawlers, find-in-page, and screen readers see the full list regardless of animation state.
  • The demo’s toggle is an sr-only checkbox named by its visible label; the focus ring is forwarded onto the label with peer-focus-visible/replay:outline-*.
  • The toggle only fades the items — they keep their space and stay in the accessibility tree. It’s a replay control, not a disclosure; use a real show/hide pattern to actually remove content.

Reduced motion

The global prefers-reduced-motion rule collapses the transition to an instant appearance, and motion-safe: removes the per-item delays entirely, so no content is ever held invisible.

The fallback story

  • Without sibling-index(), the calc() is invalid and the delay declaration is dropped — every item animates at once. The entrance still plays, just without the cascade.
  • Without @starting-style, there is no starting frame: items render already settled. You lose the animation and nothing else.

Browser support

Minimum stable version per engine, resolved at build time from MDN's browser-compat-data; Baseline status from the official web-features dataset. Everything degrades gracefully — the “when missing” column is the actual behavior, not a broken page.

FeatureChromeEdgeFirefoxSafariWhen missing
sibling-index() functionLimited availability13813826.2The computed delay is invalid, so the declaration is dropped and every item animates at the same time — the entrance still plays, just without the cascade.
Entry/exit animations (allow-discrete + @starting-style)Baseline 2024 · newly available11711712917.5Elements appear and disappear instantly. Nothing breaks — you just lose the animation.

† @supports cannot test at-rules, so the badge checks transition-behavior: allow-discrete, which co-shipped with @starting-style in every engine.