Using CSS counters - draft

Author: @debiru_R

§ Using CSS Counters

CSS counters let you set a counter for an element and display that counter. For example, it can be used to automatically assign heading numbers in a web page, to renumber an ordered list, or to display the index number of an element that matches a particular selector.

Counters are created by applying counter-reset, counter-increment and counter-set properties, and counter() and counters() functions as a value of content property. For reversed counters, the reversed() function can also be used as a value of counter-reset property.

Let's look at the actual HTML and CSS descriptions and see how the counter is used.

§Basics: Basic use of counters

Using the counter involves a combination of three steps: initializing the counter, increasing or decreasing the counter, and displaying the counter. Let's look at them one at a time.

§Display a counter

To check the status of the counter, let's try it from the display first.

§HTML

<div>
  <p>(1)</p>
  <p>(2)</p>
  <p>(3)</p>
  <p>(4)</p>
</div>

<div>
  <p>(1)</p>
  <p>(2)</p>
  <p>(3)</p>
</div>

§CSS

p::before { content: counter(num) ". "; }

The counter() function is used in the content property. This statement allows the display of a counter with the counter name num. The counter name can be any name except initial, inherit, unset, revert, or none. Here we use num as an example.

If you apply this CSS, you will see something like this.

§Result

0. (1)
0. (2)
0. (3)
0. (4)

0. (1)
0. (2)
0. (3)

§Increment a counter

Let's increment the num counter by 1.

§CSS

p { counter-increment: num 1; }
p::before { content: counter(num) ". "; }

The counter-increment property specifies the counter name followed by a numerical value. If the number is omitted, it is assumed to be 1. The following statement produces the same result as above.

p { counter-increment: num; }
p::before { content: counter(num) ". "; }

If you apply this CSS, you will see something like this.

§Result

1. (1)
2. (2)
3. (3)
4. (4)

5. (1)
6. (2)
7. (3)

§Initialize a counter

The aforementioned CSS alone would have caused the counter to increase or decrease across each div. To prevent this, always initialize the counter with the counter-reset property at the scope where you want to use the counter.

div { counter-reset: num; }
p { counter-increment: num; }
p::before { content: counter(num) ". "; }

§Result

1. (1)
2. (2)
3. (3)
4. (4)

1. (1)
2. (2)
3. (3)

The basic usage of a counter is to use a combination of the counter-reset, counter-increment, and content properties in this way. Starting from the element with the counter-reset specification, a scope is created for that counter name.

§counter() and counters()

The counters() function of the content property is useful when HTML is nested as shown below.

§HTML

<ol>
  <li>(1)
    <ol>
      <li>(1-1)</li>
      <li>(1-2)</li>
    </ol>
  </li>
  <li>(2)
    <ol>
      <li>(2-1)</li>
      <li>(2-2)</li>
    </ol>
  </li>
</ol>

§CSS

ol { counter-reset: num; }
li { counter-increment: num; }
li::marker { content: counter(num) ". "; }

Let's start with the counter() function. Such HTML and CSS will look like this.

§Result

1. (1)
    1. (1-1)
    2. (1-2)
2. (2)
    1. (2-1)
    2. (2-2)

§CSS

ol { counter-reset: num; }
li { counter-increment: num; }
li::marker { content: counters(num, "-") ". "; }

Next, try using counters() instead of counter(). If you apply this CSS, you will see something like this.

§Result

1. (1)
    1-1. (1-1)
    1-2. (1-2)
2. (2)
    2-1. (2-1)
    2-2. (2-2)

Specifying counter-reset for nested HTML automatically creates a hierarchical counter. Use the counter() function to display only the end of the hierarchy, or the counters() function to display the hierarchical counter as is, to get the desired result.

§Advanced: Detailed counter behavior

§Count up in increments of 3

§HTML

<div>
  <p>(1)</p>
  <p>(2)</p>
  <p>(3)</p>
  <p>(4)</p>
</div>

<div>
  <p>(1)</p>
  <p>(2)</p>
  <p>(3)</p>
</div>

§CSS

div { counter-reset: num; }
p { counter-increment: num 3; }
p::before { content: counter(num) ". "; }

counter-increment with 3 as the numeric value. If you apply this CSS, you will see something like this.

§Result

3. (1)
6. (2)
9. (3)
12. (4)

3. (1)
6. (2)
9. (3)

In this case, by changing the first value with counter-set, a counter that starts at 1 and increments by 3 can be represented.

§CSS

div { counter-reset: num; }
p { counter-increment: num 3; }
p:first-child { counter-set: num 1; }
p::before { content: counter(num) ". "; }

If you apply this CSS, you will see something like this.

§Result

1. (1)
4. (2)
7. (3)
10. (4)

1. (1)
4. (2)
7. (3)

§Reversed counter

Reversed counter is used to represent countdowns.

Let's look at an example that attempts to express a countdown by changing the values of counter-reset and counter-increment for the aforementioned HTML.

§CSS

div { counter-reset: num 5; }
p { counter-increment: num -1; }
p::before { content: counter(num) ". "; }

If you apply this CSS, you will see something like this.

§Result

4. (1)
3. (2)
2. (3)
1. (4)

4. (1)
3. (2)
2. (3)

The countdown is expressed, but if the number of elements differs from scope to scope, the initial value of the numbering will be off. In such cases, the reversed() function is useful.

§CSS

div { counter-reset: reversed(num); }
p { counter-increment: num -1; }
p::before { content: counter(num) ". "; }

If you apply this CSS, you will see something like this.

§Result

4. (1)
3. (2)
2. (3)
1. (4)

3. (1)
2. (2)
1. (3)

You can also change the amount of decrement during the countdown by specifying a numerical value for counter-increment, just as you would for a normal counter. by changing the last value by counter-set, a reversed counter of counter that starts at 1 and increases by 3 can be expressed.

§CSS

div { counter-reset: reversed(num); }
p { counter-increment: num -3; }
p:last-child { counter-set: num 1; }
p::before { content: counter(num) ". "; }

If you apply this CSS, you will see something like this.

§Result

10. (1)
7. (2)
4. (3)
1. (4)

7. (1)
4. (2)
1. (3)

§Changing counter style

The counter() and counters() functions accept the following arguments respectively.

  • counter(<counter-name>, <counter-style>)
  • counters(<counter-name>, <separator>, <counter-style>)

<counter-style> accepts the same values as list-style-type.

§HTML

<div>
  <p>(1)</p>
  <p>(2)</p>
  <p>(3)</p>
  <p>(4)</p>
</div>

§CSS

div { counter-reset: num; }
p { counter-increment: num; }
p::before { content: counter(num, lower-roman) ". "; }

Let's look at an example with lower-roman specified for <counter-style>. If you apply this CSS, you will see something like this.

§Result

i. (1)
ii. (2)
iii. (3)
iv. (4)

In addition to the prescribed counter style, you can use @counter-style to create your own display.

§Implicit list-item counter

In an ordered list, marker values are numbered, but these marker values can be manipulated as CSS counters. It can be manipulated by setting it against a special counter name list-item.

Internally, it behaves as if the following styles are implicitly specified

ol, ul, menu { counter-reset: list-item; }
li { counter-increment: list-item; }
li::marker { content: counter(list-item) ". "; }

This allows the marker's numbering to be counted up by a number other than 1, or to display a hierarchical counter.

Note that if counter-reset: reversed(list-item) is specified, it behaves as if li { counter-increment: list-item -1; } is implicitly specified.

§HTML

<ol>
  <li>(1)
    <ol>
      <li>(1-1)</li>
      <li>(1-2)</li>
    </ol>
  </li>
  <li>(2)
    <ol>
      <li>(2-1)</li>
      <li>(2-2)</li>
    </ol>
  </li>
</ol>

§CSS

li::marker { content: counters(list-item, "-") ". "; }

In this example, for nested ordered list HTML, the content property for li::marker is replaced with counters() instead of counter(). You can display a hierarchical counter simply by rewriting it.

§Result

1. (1)
    1-1. (1-1)
    1-2. (1-2)
2. (2)
    2-1. (2-1)
    2-2. (2-2)

§Hierarchical counter to flat HTML

You may want to perform hierarchical numbering on flat HTML. See the following example.

§HTML

<h1>(1)</h1>
<h2>(1-1)</h2>
<h2>(1-2)</h2>
<h3>(1-2-1)</h3>
<h1>(2)</h1>
<h2>(2-1)</h2>
<h2>(2-2)</h2>
<h3>(2-2-1)</h3>

Suppose we want to display the following for this HTML.

§Expected Result

1. (1)
1-1. (1-1)
1-2. (1-2)
1-2-1. (1-2-1)
2. (2)
2-1. (2-1)
2-2. (2-2)
2-2-1. (2-2-1)

To make this happen, multiple counters must be combined to represent a pseudo-hierarchical counter.

§CSS

body { counter-reset: num1 num2 num3; }
h1 { counter-increment: num1; counter-set: num2 num3; }
h2 { counter-increment: num2; counter-set: num3; }
h3 { counter-increment: num3; }
h1::before { content: counter(num1) ". "; }
h2::before { content: counter(num1) "-" counter(num2) ". "; }
h3::before { content: counter(num1) "-" counter(num2) "-" counter(num3) ". "; }

counter-set is used to set the counters of num2 and num3, which were incremented under the first h1, back to 0 when the second h1 appears, for example.

Note that counter-reset was previously used in this case, but the specification of counter-reset was changed with the introduction of the counter-set property, so now Some browsers do not produce the expected results unless counter-set is used. See Side effects of strict scope for more information.

However, some browsers may not support counter-set. For browsers that do not support counter-set, you have to use counter-reset to set the counter for lower-level headings to be set back to 0.

§Examples

§Show chapters

§HTML

<div>
  <h1>Down the Rabbit-Hole</h1>
  <h1>Pool of Tears</h1>
  <h1>A Caucus-race and a Long Tale</h1>
</div>

§CSS

div { counter-reset: section; }
h1 { counter-increment: section; }
h1::before { content: "Section " counter(section) ": "; }
h1 { font-size: 1em; }

§Actual Result

§Counting rendered elements

§HTML

<div>
  <input id="item-1" type="checkbox" checked /><label for="item-1">item-1</label>
  <input id="item-2" type="checkbox" checked /><label for="item-2">item-2</label>
  <input id="item-3" type="checkbox" checked /><label for="item-3">item-3</label>
  <input id="item-4" type="checkbox" checked /><label for="item-4">item-4</label>
  <input id="item-5" type="checkbox" checked /><label for="item-5">item-5</label>
  <table>
    <thead>
      <tr><th>count</th><th>index</th><th>value</th></tr>
    </thead>
    <tbody>
      <tr class="item-1"><td></td><td>1</td><td>Down the Rabbit-Hole</td></tr>
      <tr class="item-2"><td></td><td>2</td><td>Pool of Tears</td></tr>
      <tr class="item-3"><td></td><td>3</td><td>A Caucus-race and a Long Tale</td></tr>
      <tr class="item-4"><td></td><td>4</td><td>The Rabbit sends in a Little Bill</td></tr>
      <tr class="item-5"><td></td><td>5</td><td>Advice from a Caterpillar</td></tr>
    </tbody>
  </table>
</div>

§CSS

/* table border */
table { margin: 20px; border-collapse: collapse; }
th, td { padding: 4px 8px; border: 1px solid #999; text-align: center; }

/* filtering by input[type="checkbox"] */
tbody tr { display: none; }
#item-1:checked ~ table .item-1 { display: table-row; }
#item-2:checked ~ table .item-2 { display: table-row; }
#item-3:checked ~ table .item-3 { display: table-row; }
#item-4:checked ~ table .item-4 { display: table-row; }
#item-5:checked ~ table .item-5 { display: table-row; }

/* CSS counter */
table { counter-reset: count; }
tbody tr { counter-increment: count; }
tbody td:nth-of-type(1)::before { content: counter(count); color: red; }

§Actual Result

§Display links with empty content

§HTML

<p>See <a href="https://www.mozilla.org/"></a></p>
<p>If you want to know more about us, please refer to <a href="https://developer.mozilla.org/en-US/docs/MDN/About">About MDN Web Docs</a></p>
<p>See also <a href="https://developer.mozilla.org/"></a></p>

§CSS

:root { counter-reset: link; }
a[href] { counter-increment: link; }
a[href]:empty::before { content: "[" counter(link) "]"; }

§Actual Result

§Reverse list-item order

§HTML

<ol>
  <li>(1)
    <ol>
      <li>(1-1)</li>
      <li>(1-2)</li>
    </ol>
  </li>
  <li>(2)
    <ol>
      <li>(2-1)</li>
      <li>(2-2)</li>
    </ol>
  </li>
</ol>

§CSS

ol { counter-reset: reversed(list-item); }

§Actual Result

§Notes on CSS counters

§Relationship between ordered list numbering and CSS counters

Previously, ordered list counting was implemented independently of CSS counters, but with the introduction of counter-set, the CSS counter specification was changed and ordered list counting is now implemented in the same way as CSS counters. Only browsers that follow this revised specification can manipulate the Implicit list-item counter.

§Numbering for invalid HTML

§HTML

<ol>
  <li>(1)</li>
  <li>(2)</li>
  <ol>
    <li>(2-1)</li>
  </ol>
  <li>(3)</li>
  <li>(4)</li>
</ol>

§CSS

ol { counter-reset: num; }
li { counter-increment: num; }
li::marker { content: counter(num); }

Although we will use CSS counters here for clarity, the CSS specification assumes that the default list-item numbering will have the same result.

The <ol> element appears directly below the <ol> element. This is invalid HTML, but previously the HTML and CSS above would have rendered it as follows.

§Result

1. (1)
2. (2)
    3. (2-1)
2. (3)
3. (4)

Invalid HTML exists for various reasons and cannot be ignored (the DOM generated by document.execCommand('indent') also creates the same situation as incorrect HTML). In order to make this numbering the same as for valid HTML, the behavior of counter-reset was changed by implementing "strict scope" in some newer browsers. Browsers that implement strict scope now display the following.

§Result

1. (1)
2. (2)
    1. (2-1)
3. (3)
4. (4)

§Side effects of strict scope

§HTML

<h1>(1)</h1>
<h2>(1-1)</h2>
<h2>(1-2)</h2>
<h3>(1-2-1)</h3>
<h1>(2)</h1>
<h2>(2-1)</h2>
<h2>(2-2)</h2>
<h3>(2-2-1)</h3>

§CSS

body { counter-reset: num1 num2 num3; }
h1 { counter-increment: num1; counter-set: num2 num3; }
h2 { counter-increment: num2; counter-set: num3; }
h3 { counter-increment: num3; }
h1::before { content: counter(num1) ". "; }
h2::before { content: counter(num1) "-" counter(num2) ". "; }
h3::before { content: counter(num1) "-" counter(num2) "-" counter(num3) ". "; }

If you want to do hierarchical numbering for flat HTML, the above CSS could be used to achieve this.

§Result

1. (1)
1-1. (1-1)
1-2. (1-2)
1-2-1. (1-2-1)
2. (2)
2-1. (2-1)
2-2. (2-2)
2-2-1. (2-2-1)

Here, for browsers that implement strict scope, using counter-reset instead of counter-set would result in the following display.

§Result

1. (1)
1-1. (1-1)
1-2. (1-2)
1-2-1. (1-2-1)
2. (2)
2-3. (2-1)
2-4. (2-2)
2-4-2. (2-2-1)

Note that before counter-set was introduced, this case was implemented using counter-reset. Firefox 82 and later, which implement strict scoping, are affected by this issue.

§Implicit counter-reset: list-item non-applicability

In Implicit list-item counter, we explained that the following CSS is implicitly applied

ol, ul, menu { counter-reset: list-item; }
li { counter-increment: list-item; }
li::marker { content: counter(list-item) ". "; }

/* If counter-reset: reversed(list-item) is specified */
li { counter-increment: list-item -1; }

The CSS specification states that if you override the counter-increment property (for your own CSS counter description), it will behave as if counter-increment: list-item is applied internally.

On the other hand, there is no such arrangement for the counter-reset property. Traditionally, it behaved as if counter-reset was implicitly applied even if it was overridden (with a description for its own CSS counter), but some browsers do not implicitly apply counter-reset: list-item, and therefore the default numbering for li::marker may be broken.

§HTML

<ol>
  <li>(1)
    <ol>
      <li>(1-1)</li>
      <li>(1-2)</li>
    </ol>
  </li>
  <li>(2)
    <ol>
      <li>(2-1)</li>
      <li>(2-2)</li>
    </ol>
  </li>
</ol>

§CSS

ol { counter-reset: none; }

For browsers that do not implicitly apply counter-reset to list-item, the following will be displayed.

§Result

1. (1)
    2. (1-1)
    3. (1-2)
4. (2)
    5. (2-1)
    6. (2-2)

§Expected Result

Essentially, the expected display results are as follows.

1. (1)
    1. (1-1)
    2. (1-2)
2. (2)
    1. (2-1)
    2. (2-2)

To maintain the expected display results, you must explicitly include counter-reset for list-item.

§CSS

/* Instead of none */
ol { counter-reset: list-item; }

/* If you want to use your own CSS counter and list-item together */
ol { counter-reset: num list-item; }

Firefox 68 and later, which do not apply implicit counter-reset: list-item, are affected by this issue.

§See also