EdgeCases Logo
Feb 2026
CSS
Surface
6 min read

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.

css
sibling-index
sibling-count
css-functions
animations
layout

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 siblings
  • sibling-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

Related Insights

Explore related edge cases and patterns

CSS
Surface
CSS text-wrap: balance and pretty
6 min
CSS
Surface
CSS :has() — The Parent Selector That Changes Everything
6 min

Advertisement