The Problem with Staggered Animations (and Everything Else)
You know the drill: a grid of cards, a list of items, a set of tabs. You want them to fade in one after another, or spread evenly across a container, or distribute around a circle. Every time, you end up writing a dozen :nth-child() rules or looping through elements in JavaScript to set inline --index variables.
/* The old way: one rule per item */
li:nth-child(1) { --idx: 1; }
li:nth-child(2) { --idx: 2; }
/* ... nine more ... */
li {
animation-delay: calc(var(--idx) * 100ms);
}
Ten items, ten rules. Fifty items? You either cap it or generate hundreds of selectors at build time. JavaScript works, but it spreads layout concerns across your scripts and breaks silently when someone refactors the component.
The browser already knows which element is the third child. It built the DOM tree. CSS just couldn't access that information — until now.
Based on the original article from Smashing Magazine, here's everything you need to know about the new CSS tree-counting functions.

How sibling-index() and sibling-count() Work
sibling-index() gives you the 1-based position of an element among its parent's children. First child returns 1, fifth child returns 5. sibling-count() returns the total number of element children the parent has.
Both functions resolve to an actual number, not a string. That means you can throw them into calc(), min(), max(), round(), and even trigonometric functions like sin() and cos().
/* Staggered animation in one line */
li {
animation-delay: calc(sibling-index() * 100ms);
}
/* Reverse stagger: last item animates first */
.card {
animation: fade-in 0.4s ease both;
animation-delay: calc((sibling-count() - sibling-index()) * 80ms);
}
/* Automatic equal widths */
.tab {
width: calc(100% / sibling-count());
}
This is fundamentally different from :nth-child(), which is a selector. :nth-child() picks elements — it doesn't produce a value you can calculate with. sibling-index() sits inside your declarations and gives you a number. They solve different problems.

Real-World Patterns and Gotchas
Circular Menus
.radial-item {
--angle: calc((360deg / sibling-count()) * sibling-index());
--radius: 120px;
position: absolute;
left: calc(50% + var(--radius) * cos(var(--angle)));
top: calc(50% + var(--radius) * sin(var(--angle)));
transform: rotate(calc(var(--angle) * -1));
}
Six items? Hexagon. Eight? Octagon. Add or remove items and the layout recalculates. No JavaScript computing coordinates.
Hue Distribution
.swatch {
background-color: hsl(
calc((360deg / sibling-count()) * sibling-index()) 70% 50%
);
}
Three items get hues 120° apart. Twelve items get 30° increments. The palette adapts to whatever's in the DOM.
The Gotchas You Need to Know
- Shadow DOM scoping: These functions operate on the DOM tree, not the flattened visual tree. Inside a shadow root, they only see the shadow tree's children. Light DOM content projected via
<slot>is invisible. display: nonestill counts: Hidden elements are still in the DOM, sosibling-index()includes them. For search filters or dynamic visibility, you need to actually remove nodes or fall back to JavaScript.- Pseudo-elements don't count:
::beforeand::afteraren't siblings. But you can use these functions inside pseudo-element declarations — they evaluate against the originating element. - Custom properties evaluate immediately: If you set
--idx: sibling-index()on a parent, it resolves to the parent's index. Every child inherits that single fixed value. Apply the function directly on the elements that need it.

Browser Support and Progressive Enhancement
As of mid-2025, Chrome/Edge 138 and Safari 26.2 ship these functions. Firefox is working on it (tracked in Bugzilla #1953973). Chrome and Safari together cover roughly 75–80% of global traffic.
For production today, use @supports:
/* Baseline: works everywhere */
.item {
width: 25%;
animation-delay: 0ms;
}
/* Progressive enhancement */
@supports (z-index: sibling-index()) {
.item {
width: calc(100% / sibling-count());
animation-delay: calc(sibling-index() * 80ms);
}
}
Performance at Scale
Changing the DOM triggers style recalculation for affected siblings. For normal UI (navs, grids, tabs), you'll never notice. But for an infinite-scroll feed with thousands of nodes constantly churning, keep using JavaScript-managed indexes inside your virtualization window.
What's Coming
The CSSWG has discussed an of argument (like :nth-child() already supports) to count only siblings matching a specific selector. There's also talk of children-count() and descendant-count() functions for parent-driven layouts.
Accessibility Note
These functions are purely visual. If you use sibling-index() math to visually reorder a list, a screen reader still reads the DOM in source order. For interactive components, you still need JavaScript to sync ARIA attributes (aria-posinset, aria-setsize).
Limitations and Caveats
- No Firefox support yet: You need a fallback for ~20% of users.
display: nonebreaks sequential counting: For radial menus or proportional widths with dynamic filtering, consider removing nodes from the DOM instead of hiding them.- Shadow DOM isolation: Web component authors need to be aware of scoping limitations.
Next Steps
- Test these functions in Chrome Canary or Safari Technology Preview
- Start with
@supports-guarded progressive enhancement in side projects - Read the CSS Values and Units Module Level 5 spec for the full details
- Explore related patterns like CSS at scale with StyleX for a different approach to maintainable stylesheets
Further reading: For a deeper dive into minimizing runtime costs in game AI, check out our analysis on code agents vs tool-calling.