Ronalds Vilciņš

CSS Calculations With sibling-index() and sibling-count()

Giving every item a hand-written index just to stagger an animation feels like bookkeeping the browser could do. That’s what I like about CSS sibling-index() : it supplies the item’s position as a number, so the stylesheet can calculate the delay.

sibling-count() supplies the total number of sibling elements, including the current element. The useful part is having those numbers available inside a calculation, without maintaining another set of values in the markup.

Read a position with sibling-index()

Start with four list items:

<ul class="items">
  <li>One</li>
  <li>Two</li>
  <li>Three</li>
  <li>Four</li>
</ul>
.items > li {
  --index: sibling-index();
  --total: sibling-count();
}

The indexes run from one to four. Every item has a total of four, because the count includes the element where the function is used.

These are numeric values, so they can become inputs to calc(). Defining the custom properties alone does not change the appearance; a property still needs to use them.

Calculate animation delays from the index

Apply this CSS to the same list:

@supports (animation-delay: calc(sibling-index() * 1ms)) {
  @media (prefers-reduced-motion: no-preference) {
    .items > li {
      animation: fade-in 400ms both;
      animation-delay: calc((sibling-index() - 1) * 80ms);
    }
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(0.5rem);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Subtracting one gives the first item a zero delay. The following items wait 80ms, 160ms, and 240ms.

I would keep this animation optional. The feature query leaves it out in browsers without the function. The reduced-motion media query enables it only when the user has not requested reduced motion.

Both conditions wrap the same .items > li selector. Without either condition, the list remains visible and static.

Count all children, including hidden ones

The tree-counting rules use element siblings in the document tree. They do not count only the elements matched by your selector.

Hiding the second list item with display: none leaves the third item’s index at three. Its animation delay remains 160ms, even though it is now the second visible item.

A different element inserted among the list items also affects the numbers. Text nodes and whitespace do not count as element siblings.

The distinction worth watching is the difference between document order and what happens to be visible. I would use this calculation when those orders agree. If an animation needs consecutive delays for a filtered subset, DOM sibling positions may not match that requirement.

Divide available width between children

For the four-item list, the count can produce equal widths:

.items {
  display: flex;
  padding: 0;
  list-style: none;
}

.items > li {
  width: calc(100% / sibling-count());
}

Each item gets a quarter of the container width. This small example has no gap or item margins; those would need to be accounted for separately.

For ordinary equal-width columns, I’d stick with Flexbox’s flex property . Calculating the sibling count adds work without improving this layout:

.items > li {
  flex: 1;
}

Use that declaration in place of the calculated width. The numeric count earns its place when another calculation actually needs the total.

Place items around a circle

An angle can depend on both the total and the current position:

<div class="orbit">
  <span class="item">A</span>
  <span class="item">B</span>
  <span class="item">C</span>
  <span class="item">D</span>
</div>
.orbit {
  position: relative;
  width: 20rem;
  height: 20rem;
}

.orbit > .item {
  position: absolute;
  left: 50%;
  top: 50%;
  --angle: calc(360deg / sibling-count() * (sibling-index() - 1));
  transform: translate(-50%, -50%) rotate(var(--angle)) translateX(8rem);
}

Four items receive angles of zero, 90, 180, and 270 degrees. Absolute positioning gives them a shared centre before the transforms move them around it.

The labels rotate with their elements in this example. I like it as a small demonstration of the maths, but I would want a different approach for navigation. The fixed-size container alone makes it a poor starting point for a responsive menu.

Temani Afif’s CSS Tip examples using element indexes show other ways to use sibling numbers in visual effects.

Spread a value across a list

A position can also become a fraction between zero and one:

.items > li {
  --p: calc((sibling-index() - 1) / max(1, sibling-count() - 1));
  opacity: calc(0.4 + var(--p) * 0.6);
}

For four items, the opacities are 0.4, 0.6, 0.8, and 1. The denominator stays at least one, so a single-item list avoids division by zero and uses 0.4.

I find the fraction more useful than this particular fading effect. For an actual component, I would apply it to decoration rather than making list text harder to read. The calculation is reusable even when the visual treatment needs to change.

Keep selectors for selecting elements

The :nth-child() selector still handles styling a particular position:

.items > li:nth-child(3) {
  font-weight: bold;
}

Use a selector when the requirement is to style the third element. Use sibling-index() when the position needs to participate in arithmetic.

Check the functions your calculation uses

MDN lists both sibling-index() and sibling-count() as Baseline 2026. Older browser versions may still need a fallback, such as the static list used in the animation example.

I would happily replace hand-written index values when they only repeat the real sibling order. A manually supplied custom property still makes sense for older browsers, or when the application’s order means something different. Being able to count the children in CSS does not make the document tree the right source for every number.

Filed under and on

Enjoyed this article?

If you found it useful, share it with a friend or someone who might find it helpful too. Thanks for reading!

← Back to all articles