Version 0.1.0 just released! Check out the release notes from GitHub Releases

Stagger a List of React Elements With a Single Keyframe Plus an index Variant, Authored Once at Build Time

Staggered List Animation in React

A "staggered" list animation — items fading in one after another — is one of those effects that ends up costing more than it should. The usual approaches: hand-author N keyframes, or compute animationDelay from a JS index at render time. Salty CSS does it with one keyframe and an index variant on the list item. The CSS for all five delays is compiled up-front; rendering each item is just picking the right variant class.

Define the animation

// /styles/animations.css.ts
import { keyframes } from "@salty-css/react/keyframes";

export const fadeIn = keyframes({
  // Apply the "0%" state immediately so delayed items don't flash
  // their final state before the animation begins.
  appendInitialStyles: true,
  params: { duration: "350ms", easing: "ease-out", fillMode: "both" },
  from: { opacity: 0, transform: "translateY(8px)" },
  to: { opacity: 1, transform: "translateY(0)" },
});

The list item

// /components/staggered-item.css.ts
import { styled } from "@salty-css/react/styled";
import { fadeIn } from "../styles/animations.css";

export const StaggeredItem = styled("li", {
  base: {
    animation: fadeIn,
  },
  variants: {
    index: {
      0: { animationDelay: "0s" },
      1: { animationDelay: "0.08s" },
      2: { animationDelay: "0.16s" },
      3: { animationDelay: "0.24s" },
      4: { animationDelay: "0.32s" },
    },
  },
});

Render the list

Cap the index at the highest variant so a longer list still works — it just stops adding delay past the fifth item.

import { StaggeredItem } from "./staggered-items.css";

export const ItemsList = () => {
  const items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"];

  return (
    <ul>
      {items.map((item, index) => (
        <StaggeredItem key={item} index={Math.min(index, 4)}>
          {item}
        </StaggeredItem>
      ))}
    </ul>
  );
};

What it produces

The compiler emits one @keyframes block for fadeIn and five tiny rules — one per index value — in saltygen/index.css. No animationDelay arithmetic happens at render time; each list item just picks the variant class that matches its index prop.

Gotchas

  • Respect prefers-reduced-motion. Wrap the animation in a media query that disables it for users who opted out — see the Animations guide for the pattern.
  • appendInitialStyles: true matters. Without it, items render at their to state for the duration of the delay, then snap to the from state when the animation kicks in. With it, the starting style is applied directly, so the cascade looks clean from the first paint.
  • Cap the index. The recipe above pre-defines five variants. If your list is longer, decide whether you want the cap (looks fine for ~10+ items) or to add more variants — adding 20 variants is cheap because each is a tiny rule.