.
The inheritance of CSS-counters
According to the specification, each element has a set of counters. And for each counter in that set there are:
- a counter name as a string
- a counter creator element as a reference to the element
- a counter value as an integer
Where does this set come from?
An element inherits its initial set of counters (in addition to those declared on the element itself) from its parent and previous sibling. This set then updates values of its counters with the values of the counters with the same name of its previous element in tree order.
You can read the detailed algorithm description in the specification. But here I want to leave here the summary of it with some simplifications:
- Copy the parent's set of counters.
- If there are previous elements at the same nesting level, then take the nearest one and copy counters from there that we don’t have yet.
- Take the nearest previous element in the tree order, select by name the counters that we already have in the list, and take the new values.
From that we can get the main idea, that I want to put here:
The values of the counters can be influenced by the ancestors and siblings, but the existence of the counters themselves on this element is only affected by the ancestors.
That idea I want to explain and prove by following example:
<ul class='outer'>
<li class='outer__elem'></li>
<li class='outer__elem''></li>
<li class='outer__elem'>
<ul class='inner'>
<li class='inner__elem'></li>
<li class='inner__elem'></li>
<li class='inner__elem'></li>
</ul>
</li>
<li class='outer__elem'></li>
</ul>
There is an outer list and an inner list nested in one of the items of the outer list. Let's add styles:
/* set the counter on outer list */
ul.outer {
counter-reset: global;
}
/* increase counter on every list item */
li {
counter-increment: global;
}
/* display the counter */
li::before {
content: counter(global);
}
With this CSS the counter is incremented before each displaying, and it looks like this:
Now let's create another counter, the local one, and display them too:
/* set the counter on outer list */
ul.outer {
counter-reset: outer;
}
/* set the counter on inner list */
ul.inner {
counter-reset: inner;
}
/* increase counter on every outer list item */
li.outer__elem {
counter-increment: outer;
}
/* increase counter on every inner list item */
li.inner__elem {
counter-increment: inner;
}
/* display the counters */
li.outer__elem::before {
content: counter(outer);
}
li.inner__elem::before {
content: counter(inner);
}
Now we see this picture:
And what happens if you try to take out the internal counter in the external list?
/* display both counters on every list item */
li::before {
content: counter(outer) ', ' counter(inner);
}
The zeros are shown because when the counter()
function does not have a counter of given name in
scope, it returns 0
as the default value. It means that inner counter is not available in
the external list. And this is an important conclusion for us, which was first taken from
the specification, but now from practice.
Let's add a global counter from the first code fragment to this code and display it on the screen:
li::before {
content: counter(global) ' - ' counter(outer) ', ' counter(inner);
}
Let's turn to the algorithm from the specification at the beginning of the article and see why the inner
counter at the end is not available and how it generally works from the inside.
The inherited set of counters for the first element of the outer list looks like this:
creator | name | value |
---|---|---|
ul.outer |
global |
0 |
ul.outer |
outer |
0 |
In order not to be confused by the value of 0
, let me emphasise you that this is an inherited
set. Before displaying, the element does all the specified operations with it, in this case counter-increment:
other;
, that is, it adds 1, so we see exactly 1 on the screen.
Therefore, the inherited set for the second element of the outer list looks like this:
creator | name | value |
---|---|---|
ul.outer |
global |
1 |
ul.outer |
outer |
1 |
The inherited set of the first element of the internal list, which already has three counters:
creator | name | value |
---|---|---|
ul.outer |
global |
4 |
ul.outer |
outer |
3 |
ul.inner |
inner |
0 |
The set of the last element of the outer list immediately following the element with the inner list:
creator | name | value |
---|---|---|
ul.outer |
global |
6 |
ul.outer |
outer |
3 |
Why is there no internal counter here? Let's go through the algorithm step by step for the current element!
Step 1. We take a set of the parent and copy. This is ul.other
and its set
looks like this:
creator | name | value |
---|---|---|
ul.outer |
global |
0 |
ul.outer |
outer |
0 |
Step 2. We take the previous element of the same nesting level and copy counters from there,
whose names we do not have yet. It will be li.other__elem:nth-child(3)
and its set looks like
this:
creator | name | value |
---|---|---|
ul.outer |
global |
6 |
ul.outer |
outer |
2 |
Step 3. Of the counters that are already in the element's set after step 2, there are only
other
and global
counters here, so we copy their values.
In total: the inherited set of counters for the li.other__elem:nth-child(4)
element look like this:
creator | name | value |
---|---|---|
ul.outer |
global |
6 |
ul.outer |
outer |
3 |
As you can see, there is no way for the internal counter to get into that element.
At the end of the article, we can once again conclude that the values of the counters can be influenced by the ancestors and children of the ancestors, that is, the entire tree from above, and the presence of the counters themselves on this element only by the ancestors of this nesting level and higher ones.