sibling-index()

Mojtaba Seyedi on

Get affordable and hassle-free WordPress hosting plans with Cloudways — start your free trial today.

Experimental: Check browser support before using this in production.

The sibling-index() CSS function returns the position of an element among its siblings, starting from 1. It’s similar to :nth-child(), but instead of using it in selectors, you can now use the index directly within CSS functions and calculations.

ul li {
  transform: translateX(calc(sibling-index() * 10px));
}

Syntax

<sibling-index()> = sibling-index()

The sibling-index() function takes no arguments and returns an <integer>.

Use cases

1. Staggered transitions

A common use case is creating staggered animations. Previously, this required specifying delays for each element using :nth-child() selectors. With sibling-index(), you can achieve this only with one line of code.

To show how this works, I’m adding staggered animation to a CodePen I made nine years ago. Here is the old Pen — back from the days of FontAwesome, Facebook, and Google+.

Let’s add a 100ms (1 × 100ms) delay to the first child, 200ms (2 × 100ms) to the second, and so on.

.menu-item {
  transition-delay: calc(sibling-index() * 100ms);
}

To exclude the first element from the delay:

.menu-item {
  transition-delay: calc(sibling-index() - 1 * 100ms);
}

See it in action:

At the time, this only works behind the Experimental Web Platform features flag in Chromium-based browsers. Here is a video so you can see how it turns out:

2. Dynamic styling

You can also use sibling-index() to apply dynamic styles, such as varying background colors based on the element’s index:

.menu-item {
  background-color: hsl(calc((sibling-index() - 1) * 90), 70%, 60%);
}

In my previous examples, the menu items can have different background colors:

Here is the result if you your browser doesn’t support this function yet:

3. Positioning elements

You can get fancier, too, like laying 40 dots out in a circle by calculating angles and positions using sibling-index().

dot {
  --angle: calc(sibling-index() * 9deg); /* 360deg / 40 dots */
  --radius: 120px;
  position: absolute;
  width: 10px;
  height: 10px;
  background: hsl(calc(sibling-index() * 9), 100%, 60%);
  border-radius: 50%;
  top: calc(50% + sin(var(--angle)) * var(--radius));
  left: calc(50% + cos(var(--angle)) * var(--radius));
  transform: translate(-50%, -50%);
}

Additional examples

The use cases are endless! Knowing an element’s index can make all kinds of things easier to calculate. Here are a couple of cool examples I came across.

Andrew Bone uses sibling-index() to build a boustrophedon layout — that’s a zigzag pattern where items snake left to right, then right to left on the next row (like mowing a lawn or reading boustrophedon-style writing). He uses the element’s index to calculate which row it should land on using:

grid-row: round(up, calc(sibling-index() / 3));

This divides the index by three (for three columns), then rounds up to figure out which row the item belongs in. That gives a consistent layout even as you add or remove items. It’s all done in CSS, no JavaScript required.

Fabio Giolito uses it to calculate the right angle for each image in his carousel — super handy when you’re spreading items around a circle.

sibling-index() works on the DOM tree, not the flat tree

The specification states:

These functions, to match selectors like :nth-child(), operate on the DOM tree, rather than the flat tree like most CSS values do. They may, in the future, have variants that support counting flat tree siblings.

But what does that actually mean? Let’s look at an example of a web component to break it down:

<fancy-section>  
  <div>Slotted 1</div> 
  <div>Slotted 2</div>  
  <div>Slotted 3</div> 
</fancy-section>

<script>
  class FancySection extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' }).innerHTML = `
        <section>
          <slot></slot>
          <div>Internal</div>
        </section>
        <style>
          div {
            margin-left: calc(sibling-index() * 50px);
          }
        </style>
      `;
    }
  }

  customElements.define('fancy-section', FancySection);
</script>

At a glance, it looks like we have four <div>s visible in the page:

  1. Three coming from the light DOM and slotted into the component
  2. One internal <div> inside the shadow DOM coming from JavaScript

You might expect the sibling-index() of the internal <div> to be 4 (the fourth visible div). But it’s actually 2, and here’s why:

When the browser renders a web component, it creates two trees:

  • The DOM tree: the actual parent/child relationships in markup
  • The flat tree: what you see, with slots filled and components expanded

The sibling-index() function works on the DOM tree, which means it doesn’t see slotted content as siblings of internal elements. It only considers elements that are siblings in the same tree scope.

So, the DOM structure inside the <section> from the example is this:

<section>
  <slot></slot>        <!-- First child -->
  <div>Internal</div>  <!-- Second child -->
</section>

Only two elements exist in this shadow DOM:

  • The <slot>
  • The internal <div>

That means the internal <div> is the second sibling, and:

margin-left: calc(sibling-index() * 50px); /* 2 * 50px = 100px */

Even though it visually appears after three slotted divs, those are not its siblings in the DOM tree.

As you see, sibling-index() is tree-scoped, meaning it operates within the DOM tree that the element belongs to and won’t cross boundaries like Shadow DOM or slots. When used on pseudo-elements or in different tree contexts (such as ::part, ::slotted, or :host), it resolves based on the originating real element within the same tree. If the rule and the element come from different trees, the function resolves to 0 to avoid leaking internal structure. Here are some examples borrowed from the specifications:

#target::before {
  /* Based on the sibling-index() of #target */
  width: calc(sibling-index() * 10px);
}
#target::before::marker {
  /* Based on the sibling-index() of #target */
  width: calc(sibling-index() * 10px);
}
::slotted(*)::before {
  /* Based on the sibling-index() of the slotted element - outer tree */
  width: calc(sibling-index() * 10px);
}
::part(my-part) {
  /* 0px - inner tree */
  width: calc(sibling-index() * 10px);
}
:host {
  /* Based on the hosts sibling-index() - outer tree */
  width: calc(sibling-index() * 10px);
}

Future possibility: Filter siblings with the of selector

The specification also states:

These functions may, in the future, be extended to accept an of <complex-real-selector-list> argument, similar to :nth-child(), to filter on a subset of the children.

That means you might be able to do something like this in the future:

li {
  transform: translateY(calc(sibling-index(of .on-sale) * 10px));
}

In this example, only list items with the .on-sale class are counted when calculating the index. So, the first .on-sale item gets 10px, the second gets 20px, and so on — regardless of where they are in the full list.

<ul>
  <li>Regular item</li>
  <li class="on-sale">Sale item 1</li>
  <li>Regular item</li>
  <li class="on-sale">Sale item 2</li>
  <li class="on-sale">Sale item 3</li>
</ul>

Without the of filter, Sale item 2 would be considered the fourth sibling. But with sibling-index(of .on-sale), it becomes the second filtered sibling — a more useful number when styling only specific types of elements.

Specification

The sibling-index() function is part of CSS Values and Units Module Level 5, which is currently in Working Draft status at the time of writing. That means a lot can change between now and when the feature becomes a formal Candidate Recommendation for implementation.

Browser support

As of June 2025, sibling-index() is only supported in Chromium-based browsers (like Chrome and Edge), and even then, you’ll need to enable the Experimental Web Platform features flag at chrome://flags to try it out. It’s not production-ready yet, but it’s a great time to experiment and explore what it can do.

Track the progress of the Chrome’s implementation over at Chrome Platform Status. You can check Firefox’s work in Bugzilla Ticket #1953973.

More information