Mastering the CSS sibling-index() Function: Dynamic Indexing Without JavaScript
For decades, one of CSS's most frustrating limitations was styling elements based on their position in a list without hardcoding rules.
If you wanted staggered animation delays or dynamic color gradients across a set of child elements, your choices were limited:
- Write dozens of repetitive
:nth-child(1),:nth-child(2),:nth-child(3)declarations. - Generate them with a CSS preprocessor like Sass using
@for $i from 1 through 20. - Inject inline custom properties like
style="--i: 0"through JavaScript or server-side templates.
With the CSS Values and Units Module Level 5, CSS introduces native solutions to this long-standing challenge: sibling-index() and its companion sibling-count().
1. What is sibling-index()?
The sibling-index() function returns an <integer> representing the position of the current element among its sibling elements under the same parent.
.item {
animation-delay: calc(sibling-index() * 100ms);
}
Core Characteristics:
- 1-Based Indexing: The first element returns
1, the second returns2, the third returns3, and so on. - Element Nodes Only: It exclusively counts element siblings. Text nodes, comments, and whitespace are completely ignored.
- Zero Arguments: The function takes no parameters:
sibling-index(). - Integer Return Value: Unlike
counter(), which returns formatted text for pseudo-elementcontent,sibling-index()returns a mathematical integer. This means you can plug it directly intocalc(),min(),max(),clamp(), and color functions likehsl()oroklch().
2. Meet Its Partner: sibling-count()
Along with sibling-index(), the specification introduces sibling-count(), which returns an <integer> representing the total number of sibling elements within the parent (including the element itself).
.card {
/* Distribute items equally across total siblings */
width: calc(100% / sibling-count());
}
Together, sibling-index() and sibling-count() allow relative calculations:
$$\text{Progress} = \frac{\text{sibling-index}()}{\text{sibling-count}()}$$
This ratio gives you the exact normalized position ($0.0 \dots 1.0$) of every element across any dynamic collection!
3. Practical Use Cases & Code Examples
A. Staggered Entrance Animations
Instead of writing 15 :nth-child overrides, you can stagger entrance timings with a single line:
/* Before: Manual nth-child boilerplate */
.list-item:nth-child(1) { animation-delay: 0ms; }
.list-item:nth-child(2) { animation-delay: 80ms; }
.list-item:nth-child(3) { animation-delay: 160ms; }
/* ...repeat forever */
/* After: Single expressive calculation */
.list-item {
animation: slideIn 0.5s ease-out both;
animation-delay: calc((sibling-index() - 1) * 80ms);
}
B. Dynamic Color Distribution Across Children
Want every item in a list or tab set to have an evenly spaced hue around the color wheel without hardcoding classes?
.badge {
/* Distribute hue (0deg - 360deg) based on index and total count */
background-color: hsl(
calc(sibling-index() * (360deg / sibling-count()))
75%
50%
);
}
If you have 4 badges, they receive hues of $90^\circ, 180^\circ, 270^\circ, 360^\circ$. If you add a 5th badge, CSS automatically redistributes all 5 to $72^\circ, 144^\circ, 216^\circ, 288^\circ, 360^\circ$!
C. Overlapping Stack (Card Deck Effect)
You can calculate z-index and positional offsets mathematically:
.stacked-card {
position: absolute;
top: 0;
left: 0;
/* Layer later cards above earlier ones */
z-index: sibling-index();
/* Cascade each card downward and rightward */
transform: translate(
calc((sibling-index() - 1) * 12px),
calc((sibling-index() - 1) * 8px)
);
}
4. Live Interactive Demo
Try the interactive widget below. Use the buttons to add or remove elements or re-trigger the entrance animation. Notice how the left border hue and entrance delays recalculate automatically based on the items' dynamic positions in the DOM:
-
Item 1
Initial Item Alpha
Rendered at sibling index 1 -
Item 2
Second Item Beta
Rendered at sibling index 2 -
Item 3
Third Item Gamma
Rendered at sibling index 3 -
Item 4
Fourth Item Delta
Rendered at sibling index 4
5. How Does It Compare to CSS Counters?
A common question is: “How is sibling-index() different from counter()?”
| Feature | counter() |
sibling-index() |
|---|---|---|
| Output Type | <string> |
<integer> |
| Allowed Properties | content in pseudo-elements |
Any property accepting integer / <calc()> |
| Setup Required | Requires counter-reset & counter-increment |
None (Automatic) |
| Mathematical Usage | ❌ Cannot be used in calc() |
✅ Fully supported in calc(), min(), max() |
While counter() is built for displaying numbering in text, sibling-index() provides numerical coordinates directly to the layout and rendering engine.
6. Progressive Enhancement Strategy
To start using sibling-index() today while supporting older clients, combine @supports with CSS custom property fallbacks:
/* Fallback: manual or server-injected custom property */
.card {
--index: var(--sibling-fallback-index, 1);
animation-delay: calc(var(--index) * 100ms);
}
/* Modern enhancement */
@supports (animation-delay: calc(sibling-index() * 1s)) {
.card {
--index: sibling-index();
}
}
Conclusion
The sibling-index() and sibling-count() functions represent another massive milestone in making CSS truly dynamic and declarative. By shifting positional awareness into CSS stylesheets, we eliminate brittle JavaScript layout observers, wipe out preprocessor generation loops, and write cleaner, more resilient frontend components.