CSS sibling-index() and sibling-count(): Dynamic Sibling Styling
Finally—style elements based on their position among siblings without JavaScript. Staggered animations, rainbow colors, and dynamic layouts in pure CSS.
Staggered animations, rainbow-colored lists, circular layouts, pyramid widths—all required
JavaScript or hacky :nth-child() workarounds. CSS sibling-index()
and sibling-count() change everything. These functions return an element's
position among siblings and the total sibling count, directly in CSS. Pure styling logic,
zero JavaScript.
The Functions
Both functions take no parameters and return integers:
/* sibling-index(): Position among siblings (1-indexed) */
li:first-child → sibling-index() = 1
li:nth-child(3) → sibling-index() = 3
li:last-child → sibling-index() = n (where n = total siblings)
/* sibling-count(): Total number of siblings including self */
ul with 5 li elements → sibling-count() = 5 (for each li)
These work inside calc(), making them composable with any CSS value.
Staggered Animations Without JavaScript
The classic "fade in one by one" effect traditionally required JavaScript to set
animation-delay on each element. Now it's pure CSS:
/* Each item fades in 200ms after the previous */
.card {
animation: fade-in 0.5s ease forwards;
animation-delay: calc(sibling-index() * 200ms);
opacity: 0;
}
@keyframes fade-in {
to { opacity: 1; transform: translateY(0); }
}
/* First card: 200ms delay
Second card: 400ms delay
Third card: 600ms delay
... and so on */
No querySelectorAll, no loop, no data attributes. The browser handles it.
Dynamic Column Widths
sibling-count() enables truly dynamic layouts that adapt to content:
/* Each item takes exactly 1/n of the container width */
.flex-item {
width: calc(100% / sibling-count());
}
/* 3 items → each is 33.33%
5 items → each is 20%
7 items → each is 14.28%
Works regardless of how many items you have */Add or remove items dynamically, and widths adjust automatically. Previously this required CSS custom properties set via JavaScript or container queries with fixed breakpoints.
Rainbow Colors From Math
Combine both functions with hsl() for automatic color distribution:
/* Distribute colors evenly around the color wheel */
.rainbow-item {
background-color: hsl(
calc(360deg / sibling-count() * sibling-index())
70%
50%
);
}
/* 6 items: 60°, 120°, 180°, 240°, 300°, 360° (full rainbow)
12 items: 30° increments (finer gradient)
The math scales automatically */This is particularly useful for data visualizations, tag clouds, or any UI where you need distinct colors per item without hardcoding them.
Pyramid and Triangle Layouts
Create pyramid-shaped layouts where each row grows:
/* Pyramid: each item wider than the previous */
.pyramid-item {
width: calc(sibling-index() * 50px);
margin: 0 auto;
}
/* Item 1: 50px
Item 2: 100px
Item 3: 150px
Creates a visual pyramid */
/* Inverted: largest first */
.inverted-pyramid-item {
width: calc((sibling-count() - sibling-index() + 1) * 50px);
}Circular Positioning
Position elements in a circle using trigonometric functions:
/* Place items in a circle */
.circle-container {
position: relative;
width: 300px;
height: 300px;
}
.circle-item {
--angle: calc(360deg / sibling-count() * sibling-index());
position: absolute;
left: calc(50% + 100px * cos(var(--angle)));
top: calc(50% + 100px * sin(var(--angle)));
transform: translate(-50%, -50%);
}
/* Items automatically arrange in a perfect circle
Add/remove items and the circle adjusts */
Combined with the new CSS sin() and cos() functions, you can
create radial layouts entirely in CSS.
The Gotchas
Siblings Only, Not Descendants
These functions count direct siblings—elements that share the same parent. Nested elements don't affect the count:
<ul>
<li>1</li> <!-- sibling-index() = 1, sibling-count() = 3 -->
<li>
2
<ul>
<li>2.1</li> <!-- sibling-index() = 1, sibling-count() = 2 -->
<li>2.2</li> <!-- sibling-index() = 2, sibling-count() = 2 -->
</ul>
</li>
<li>3</li> <!-- sibling-index() = 3, sibling-count() = 3 -->
</ul>All Siblings Count, Not Just Matched Ones
The count includes all sibling elements, regardless of your selector:
<div>
<span>...</span> <!-- sibling-index() = 1 -->
<p>...</p> <!-- sibling-index() = 2 -->
<span>...</span> <!-- sibling-index() = 3 -->
</div>
/* Even if you select only spans, sibling-count() = 3 */
span {
/* sibling-count() returns 3, not 2 */
background: hsl(calc(120deg * sibling-index()) 50% 50%);
}
This is consistent with how :nth-child() works, but can be surprising
if you expect filtered counts.
No Firefox Support (Yet)
As of early 2026, Firefox hasn't shipped these functions. Browser support:
- Chrome/Edge: 138+
- Safari: 26.2+
- Firefox: Not supported (behind flag in Nightly)
For cross-browser production use, provide fallbacks or use progressive enhancement. The functions simply won't apply in unsupported browsers—no errors, just default values.
Replacing JavaScript Patterns
These CSS functions replace several common JavaScript patterns:
/* BEFORE: JavaScript */
document.querySelectorAll('.item').forEach((el, i, arr) => {
el.style.setProperty('--index', i + 1);
el.style.setProperty('--total', arr.length);
});
/* AFTER: Pure CSS */
.item {
/* sibling-index() and sibling-count() are available directly */
animation-delay: calc(sibling-index() * 100ms);
width: calc(100% / sibling-count());
}No more data attributes, no more CSS custom properties set via JavaScript, no more re-running scripts when DOM changes.
Combining With CSS @function
If you're using the new CSS @function feature (also landing in 2026),
you can create reusable patterns:
@function --stagger-delay(--base: 100ms) {
result: calc(sibling-index() * var(--base));
}
@function --distribute-hue(--saturation: 70%, --lightness: 50%) {
result: hsl(
calc(360deg / sibling-count() * sibling-index())
var(--saturation)
var(--lightness)
);
}
/* Usage */
.animated-item {
animation-delay: --stagger-delay(150ms);
}
.colorful-item {
background: --distribute-hue(80%, 45%);
}Key Takeaways
sibling-index()returns 1-indexed position among siblingssibling-count()returns total sibling count including self- Both work inside
calc()for dynamic values - Counts ALL siblings, not just selector-matched ones
- Replaces JavaScript patterns for staggered animations, dynamic layouts
- Chrome/Safari support in 2026; Firefox still pending
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement