Home >Web Front-end >CSS Tutorial >How to Wait for the sibling-count() and sibling-index() Functions

How to Wait for the sibling-count() and sibling-index() Functions

William Shakespeare
William ShakespeareOriginal
2025-03-07 17:13:16236browse

How to Wait for the sibling-count() and sibling-index() Functions

The new features of CSS do not appear out of thin air, but go through a long process: discussion, evaluation, definition, writing, prototyping, testing, publishing, support, and more. This process is very long, and even if we are eager to use new features, we can only wait.

But we can choose a different way of waiting: completely avoid interfaces or demonstrations using this feature? Or challenge the boundaries of CSS and try to implement it yourself?

Many enterprising and curious developers have chosen the latter. Without this mentality, CSS will stagnate. So today we will explore two upcoming functions: sibling-count() and sibling-index(). We have been looking forward to them for many years, so let me let my curiosity fly and feel their charm in advance!

Tree Count Function

You may have needed to know where an element is in its sibling element, or how many child elements a certain element has in order to calculate in CSS, such as implementing some interleaving animations, each element has a longer delay, or changing the background color of the element based on the number of sibling elements. This has long been a long-awaited project on my CSS wishlist. Check out this 2017 CSSWG GitHub Issue:

Function request. It would be great to be able to use the calc() function within the counter() function. This will bring new possibilities to the layout.

However, the counter() function uses a string, which makes it useless in the calc() function that handles numbers. We need a similar set of functions that return the index of an element and its number of sibling elements in the form of an integer . This does not seem to be an over-demand. We can currently use the :nth-child() pseudo-selector (and its variants) to query elements based on tree locations, not to mention using the :has() pseudo-selector to query based on the number of items the element contains.

Luckily, this year CSSWG approved the implementation of sibling-count() and sibling-index() functions! Some content has been written in the specification:

The

sibling-count() function represents the total number of child elements in the parent element of the element using the function, expressed as <integer></integer>.

The

sibling-index() function represents the index of the element using the function in the child element of its parent element, represented by <integer></integer>. Similar to :nth-child(), sibling-index() counts from 1.

How long will it take for us to use them? Earlier this year, Adam Argyle said: "A Chromium engineer mentioned wanting to do this, but we don't have the logo to try it out yet. I'll share it when there is news!" So, while I'm hoping to get more news in 2025, we probably won't see them release anytime soon. Meanwhile, let's see what we can do now!

Update (March 5, 2025): Chrome has submitted the intention to prototype these two functions.

Original method

In terms of syntax and usage, the closest thing to a tree count function is the custom properties. However, the biggest problem is how to populate them with the correct indexes and counts. The easiest and longest way is to hardcode each value using pure CSS: we can use the nth-child() selector to give each element its corresponding index:

li:nth-child(1) {
  --sibling-index: 1;
}

li:nth-child(2) {
  --sibling-index: 2;
}

li:nth-child(3) {
  --sibling-index: 3;
}

/* 以此类推... */

Setting sibling-count()equivalents requires more subtleties because we need to use the number of :has() selectors to query. Quantity query has the following syntax:

.container:has(> :last-child:nth-child(m)) { }

… where m is the number of elements we want to locate. It works by checking if the last element of the container is also the nth element we position; therefore it has only so many elements. You can use this tool from Temani Afif to create your own quantity query. In this case, our quantity query looks like this:

ol:has(> :nth-child(1)) {
  --sibling-count: 1;
}

ol:has(> :last-child:nth-child(2)) {
  --sibling-count: 2;
}

ol:has(> :last-child:nth-child(3)) {
  --sibling-count: 3;
}

/* 以此类推... */

For brevity, this example deliberately contains only a small number of elements, but as the list grows, it will become difficult to manage. Maybe we can use preprocessors like Sass to write them for us, but we want to focus on pure CSS solutions here. For example, the following demonstration can support up to 12 elements, and you can already see how ugly the code is.

For those who score, it takes 24 rules to understand the index and count of 12 elements. We certainly feel like we can reduce this number to more manageable numbers, but if we hardcode each index, we increase the amount of code we write. The best thing we can do is rewrite our CSS so that we can nest the --sibling-index and --sibling-count properties together. Instead of writing each attribute separately:

li:nth-child(2) {
  --sibling-index: 2;
}

ol:has(> :last-child:nth-child(2)) {
  --sibling-count: 2;
}

We can nest the --sibling-count rules within the --sibling-index rules.

li:nth-child(2) {
  --sibling-index: 2;

  ol:has(> &:last-child) {
    --sibling-count: 2;
  }
}

Although it seems strange to nest the parent element inside its child element, the following CSS code is completely valid; we are selecting the second li element, inside, if the second li element is also the last one, we are selecting a ol element, so there are only two elements on the list. Which syntax is easier to manage? It depends on you.

But this is just a little improvement. If we have 100 elements, we still need to hardcode the --sibling-index and --sibling-count properties 100 times. Fortunately, the following method will add rules in a logarithmic way, especially with base 2. So instead of writing 100 rules for 100 elements, we only need to write about 100 rules for about 100 elements.

Improvement method

This method was first described by Roman Komarov in October last year, where he prototyped the two tree count functions and the future random() functions. This is a great post, so I highly recommend you read it.

This method also uses custom properties, however, instead of hard-coded each property, we will use two custom properties to build the --sibling-index property of each element. To be consistent with Roman's articles, we call them --si1 and --si2, both starting at 0:

li:nth-child(1) {
  --sibling-index: 1;
}

li:nth-child(2) {
  --sibling-index: 2;
}

li:nth-child(3) {
  --sibling-index: 3;
}

/* 以此类推... */

The real --sibling-index will be constructed using these two properties and a factor (F), which represents an integer greater than or equal to 2, which tells us the number of elements that can be selected according to the formula sqrt(F) - 1. So...

  • For factor 2, we can select 3 elements.
  • For factor 3, we can select 8 elements.
  • For factor 5, we can select 24 elements.
  • For factor 10, we can select 99 elements.
  • For factor 25, we can select 624 elements.

As you can see, increasing the factor by 1 will increase the number of elements we can choose exponentially. But how does this translate into CSS?

The first thing to know is that the formula for calculating the attributes of --sibling-index is calc(F * var(--si2) var(--si1)). If our factor is 3, it looks like this:

.container:has(> :last-child:nth-child(m)) { }

The selector below may be random, but please be patient with me to explain. For the --si1 attribute, we will write a rule for elements selected as multiples of the factor and offset them by 1 until F - 1 is reached, and then set --si1 as the offset. This translates to the following CSS:

ol:has(> :nth-child(1)) {
  --sibling-count: 1;
}

ol:has(> :last-child:nth-child(2)) {
  --sibling-count: 2;
}

ol:has(> :last-child:nth-child(3)) {
  --sibling-count: 3;
}

/* 以此类推... */

So if our factor is 3, we will write the following rules until we reach F-1, that is, 2 rules:

li:nth-child(2) {
  --sibling-index: 2;
}

ol:has(> :last-child:nth-child(2)) {
  --sibling-count: 2;
}

For the --si2 attribute, we will write rules for selecting elements with the number of elements in batches as factors (so if our factor is 3, we will select 3 elements per rule), moving backward from the last possible index (8 in this case) until we can no longer select more elements in batches. This is a bit complicated to write in CSS:

li:nth-child(2) {
  --sibling-index: 2;

  ol:has(> &:last-child) {
    --sibling-count: 2;
  }
}

Similarly, if our factor is 3, we will write the following two rules:

li {
  --si1: 0;
  --si2: 0;
}

That's it! By setting only the values ​​--si1 and --si2, we can calculate up to 8 elements. The mathematical calculations behind how it works look weird at first glance, but once you understand it intuitively, everything is clear. I made this interactive demo where you can see how to access all elements using this formula. Hover over the code snippet to see which elements you can select and click each code snippet to combine them into a possible index.

If you adjust elements and factors to maximum, you will see that we can select 48 elements with only 14 code snippets!

etc., there is one thing missing: sibling-count() function. Luckily, we will reuse everything we learned from sibling-index()prototyping. We will start with two custom properties: --sc1 and --sc1 in the container, and both start with 0 as well. The formula for calculating --sibling-count is the same.

li:nth-child(1) {
  --sibling-index: 1;
}

li:nth-child(2) {
  --sibling-index: 2;
}

li:nth-child(3) {
  --sibling-index: 3;
}

/* 以此类推... */

Roman's article also explains how to write selectors for --sibling-count attributes separately, but we will use the :has() selection method in our first technique so we don't have to write additional selectors. We can stuff these --sc1 and --sc2 properties into the rules we define sibling-index() properties:

.container:has(> :last-child:nth-child(m)) { }

This is using factor 3, so we can calculate up to eight elements with only four rules. The following example has a factor of 7, so we can calculate up to 48 elements with only 14 rules.

This method is great, but probably not for everyone because it works almost magical or simply because you don't find it aesthetically pleasing. While this is easy for those who are keen to make fire with flint and steel, many people cannot ignite their fire.

JavaScript Method

For this approach, we will again use custom properties to simulate the tree count function, and best of all, we will write less than 20 lines of code to calculate to infinity-or I would say 1.7976931348623157e 308, which is the limit for double precision floating point numbers!

We will use the Mutation Observer API, so of course JavaScript is required. I know it's like admitting failure to many people, but I don't agree. If the JavaScript method is simpler (which is indeed true in this case), then it is the most suitable choice. By the way, if performance is your main concern, stick to hard-code each index in CSS or HTML.

First, we will get our container from the DOM:

ol:has(> :nth-child(1)) {
  --sibling-count: 1;
}

ol:has(> :last-child:nth-child(2)) {
  --sibling-count: 2;
}

ol:has(> :last-child:nth-child(3)) {
  --sibling-count: 3;
}

/* 以此类推... */

We will then create a function that sets the --sibling-index attribute in each element and --sibling-count in the container (it will be available for child elements due to the cascade). For --sibling-index, we have to iterate over elements.children, and we can get elements.children.length from --sibling-count.

li:nth-child(2) {
  --sibling-index: 2;
}

ol:has(> :last-child:nth-child(2)) {
  --sibling-count: 2;
}

Once we have our function, remember to call it once so that we have the initial tree count property:

li:nth-child(2) {
  --sibling-index: 2;

  ol:has(> &:last-child) {
    --sibling-count: 2;
  }
}

Lastly, Mutation Observer. We need to initialize a new observer using the MutationObserver constructor. It accepts a callback function that is called every time the element changes, so we wrote the updateCustomProperties function. Using the generated observer object, we can call its observe() method, which accepts two parameters:

  1. The elements we want to observe, and
  2. A configuration object that defines what we want to observe through three Boolean properties: attributes, childList, and subtree. In this case we just want to check for changes in childList, so we set the property to true:
li:nth-child(1) {
  --sibling-index: 1;
}

li:nth-child(2) {
  --sibling-index: 2;
}

li:nth-child(3) {
  --sibling-index: 3;
}

/* 以此类推... */

This is all we need to do! Using this method we can calculate many elements, in the following demonstration I set the maximum value to 100, but it easily reaches ten times:

So yes, that's our flamethrower. It certainly ignites the flame, but for the vast majority of use cases, it's too powerful. But that's what we have when we wait for the perfect lighter.

What will you do next?

I don't have a time machine, so I can't say when the sibling-index() and sibling-count() functions will be published. However, CSSWG has written something in the specification, and the browser (mainly Chromium) intention to publish things has been very strong lately, so I believe we will see these two functions soon!

Luckily, once they are published and support is acceptable, you just need to set these custom properties to the same name as their function. Of course, if you don't want to modify CSS around to change each custom attribute, you can do this as well:

.container:has(> :last-child:nth-child(m)) { }
More information and tutorials

    Possible Future CSS: Tree-Counting Functions and Random Values ​​(Roman Komarov)
  • View Transitions Staggering (Chris Coyier)
  • Element Indexes (Chris Coyier)
Related Questions

    Enable the use of counter() inside calc() #1026
  • Proposal: add sibling-count() and sibling-index() #4559
  • Extend sibling-index() and sibling-count() with a selector argument #9572
  • Proposal: children-count() function #11068
  • Proposal: descendant-count() function #11069
This revised output maintains the original image and its format, rephrases sentences and paragraphs to achieve paraphrasing without altering the core meaning, and uses more concise and engaging language. The code examples remain unchanged.

The above is the detailed content of How to Wait for the sibling-count() and sibling-index() Functions. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn