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.

There are two types of counters: incrementing counters, meaning they count up, and reversed, or decremental, counters that count down.

Counters are essentially variables managed by CSS, both for CSS counters and for ordered list numbers. Counters can be increased or decreased by any value, can be used to define multiple named counters, and can be used to manipulate list-item counters that are automatically generated by default for ordered lists.

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.

This article explains the basic usage of counters and the implementation status in each browsers.

§ Basic counter usage

To use a counter, you must first initialize it with the counter-reset property.

§ Normal counter

§ 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>

In order to understand the behavior of counter-reset, counter-set and counter-increment, let's first try to display the counter value without specifying them. You can use any counter name you like, as long as it is not initial, inherit, unset, revert or none. In this example, we will use a counter named num.

§ CSS-1

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

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

§ Result-1

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

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

Next, let's specify counter-increment.

§ CSS-2

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

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

§ Result-2

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

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

The numbering has jumped over the div. Let's specify counter-reset so that the numbering is independent in div.

§ CSS-3

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

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

§ Result-3

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

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

The basic usage of counters is to use a combination of counter-reset, counter-increment and content in this way. Starting from an element that has counter-reset specified, a scope is created for that counter name. Make sure to specify counter-reset on the element corresponding to the scope where you want the counter serial number to be independent.

You can also change the amount of increment when counting up by specifying the counter-increment value.

§ CSS-4

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

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

§ Result-4

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

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

In this case, by changing the first value in counter-set, you can represent a counter that starts at 1 and increases by 3.

§ CSS-5

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-5

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

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

§ counter() and counters()

For nested HTML, counter-reset can be used to represent a nested counter.

§ 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-1

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

Such HTML and CSS will be displayed as follows.

§ Result-1

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

Here, counters() can be used instead of counter() to display nested counters. Look at the following example.

§ CSS-2

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

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

§ Result-2

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

If you want to change the markers in the ordered list to nested counters, etc., please refer to the Implicit list-item counter described below.

§ Reversed counter

Reversed counter is used to represent a countdown.

§ 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>

First, let's look at an example that attempts to represent a countdown by simply changing the values of counter-reset and counter-increment.

§ CSS-1

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-1

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

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

The countdown is represented, but if the number of elements in each scope is different, the initial value of the numbering will be shifted. The reversed() function is useful in such cases.

§ CSS-2

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-2

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-3

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-3

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

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

§ Changing counter style

counter() and counters() functions accept the arguments counter(<counter-name>, <counter-style>) and counters(<counter-name>, <string>, <counter-style>), respectively. <counter-style> accepts the same value as list-style-type.

§ HTML

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

Let's look at an example where lower-roman is specified for <counter-style>.

§ CSS

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

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 styles, you can use @counter-style to create your own display.

§ Detailed behavior of CSS counters

§ Nested counters and scope

One difference between counter-reset and counter-set is that counter-reset generates nested counters. See the following example.

§ 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: counters(num, "-") ". "; }

Such HTML and CSS will be displayed as follows.

§ Result

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

In this way, counters may nest counters with the same name. When a new counter is instantiated on an element, the new counter is created nested within the existing counter if it inherits the same name from its parent. The reason you see 1-2. followed by 2. instead of 1-3. in the above display is that the counters are scoped on a nested basis. counter-reset can create nested counters, so the above code will produce the expected nested counting results.

In the above example, if ol { counter-reset: num; } were written as ol { counter-set: num; }, it would display as follows.

§ Result

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

§ Flat counters

You may want to perform nested numbering for flat HTML elements. 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.

§ 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, CSS must be written in combination with counter-set as follows. The reason is that counter-reset creates a new scope in Firefox 82 and later, when the specification was changed as described below, so it is not possible to reset the counter in the current scope again. To reset the counter for the current scope, you have to use counter-set.

§ 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) ". "; }

Previously, writing counter-set as counter-reset would produce the expected result. However, this behavior was changed for the convenience of implementing the remedy for invalid HTML described below. Starting with Firefox 82, writing counter-reset for the above counter-set will display the following.

§ 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)

§ Counters for invalid HTML

According to the CSS specification, the numbering of list-item markers for ordered lists is calculated by a CSS counter. For simplicity, we will use our own counter here, but the default list-item marker should have the same result. See the following example.

§ 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); }

The ol element appears directly below the ol element. This is invalid HTML, but previously the HTML and CSS above would have displayed the following.

§ Result

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

Invalid HTML exists for a variety of reasons and cannot be ignored (and, by golly, the DOM generated by document.execCommand('indent') creates the same situation as invalid HTML). In order to make this numbering the same as for valid HTML (where the nested ol element is a child of the preceding li element), the behavior of counter-reset has been changed since Firefox 82, and now it looks like this.

§ Result

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

As described in Implicit list-item counters below, if the content property for li::marker is rewritten using counter() instead of counters(), the marker of a nested li element, even in invalid HTML, will look like this It will be displayed as a nested counter.

§ Result

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

§ Implicit list-item counter

In an ordered list, marker values are numbered, but these marker values can be manipulated as counters.

Internally, it behaves as if ol, ul, menu { counter-reset: list-item; }, li { counter-increment: list-item; } and li::marker { content: counter(list-item) ". "; } is specified. This allows the marker's number to be counted up by a number other than 1, or to display nested numbers.

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 a simple HTML, the content property for li::marker can be rewritten using counters() instead of counter() to display the nested numbering.

§ Result

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

Note that the CSS specification states that even if the counter-increment property is overridden (with a description for a unique counter), it is assumed that counter-increment: list-item is applied internally. On the other hand, there is no such arrangement for the counter-reset property. Previously, it behaved as if counter-increment: list-item was implicitly applied even if it was overridden (by a description for a custom counter), but starting with Firefox 68, counter-increment: list-item is no longer implicitly applied, and li::marker is broken (because counter-reset: list-item is no longer specified) as follows.

§ Result

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

If you want to use your own counter and the default marker numbering at the same time, you can use your own counter without breaking the marker numbering by specifying an implicit list-item name like counter-reset: my-counter list-item.

§ 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; }

§ 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 counters */
table { counter-reset: count; }
tbody tr { counter-increment: count; }
tbody td:nth-of-type(1)::before { content: counter(count); color: red; }

§ 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) "]"; }

§ 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); }

§ Result

§ Browser implementation

§ CSS counter scope/inheritance is compatible with HTML ordinals

This is about whether or not the numbering of list-item markers in an ordered list matches the numbering by the CSS counter. In the past, Counters for invalid HTML did not match the list-item marker and the CSS counter. See the following example.

§ HTML

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

<div>
  <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>
</div>

§ CSS

ol { counter-reset: list-item num; }
li { counter-increment: num; }
li::before { content: counter(num) ". "; }
li::before { color: red; }

div { counter-reset: num1 num2 num3; }
h1 { counter-increment: num1; counter-reset: num2 num3; }
h2 { counter-increment: num2; counter-reset: 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) ". "; }
h1, h2, h3 { margin: 0; font-size: 1em; font-weight: normal; }

Such HTML and CSS would display as follows The CSS in the second example above uses counter-reset instead of counter-set.

§ Result

The result of displaying an ordered list, the numbering by default marker and the numbering by CSS counter ::before pseudo-element did not match in the past. In Firefox 82, the specification has been changed to make the scope of counter-reset strict so that they match.

As a side effect of this, we could conventionally use only counter-reset to represent nested counters against flat HTML, but the strict scope of counter-reset made the numbering in the second example unnatural.

See also Flat counters for more details.

§ Does not apply implicit counter-reset: list-item

It is about whether or not it behaves as if counter-reset: list-item is not implicitly applied if you override counter-reset property value with a description for your own counter. See the following example.

§ 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; }

§ Result

Firefox 68 introduces counter-set and revamps the internal implementation of CSS counters, among other things, and obsoleted the implicit application of counter-reset. As a result, if the counter-reset property is specified, the ordered list numbering will be broken unless the list-item property value is explicitly specified. Previously, counter-reset: list-item behaved as if it were implicitly applied.

See also Implicit list-item counter for more details.

§ Browser compatibility

Desktop Mobile
Chrome Edge Firefox Internet Explorer Opera Safari WebView Android Chrome Android Firefox for Android Opera Android Safari on iOS Samsung Internet
counter-reset 2 12 1 8 9.2 3 1 18 25 10.1 1 1.0
counter-increment 2 12 1 8 9.2 3 1 18 25 10.1 1 1.0
counter() 1 12 1 8 9.2 3 1 18 4 10.1 1 1.0
counters() 1 12 1.5 8 10 3 1 18 4 10.1 1 1.0
@counter-style 91 91 33 No 77 No 91 91 33 64 No 16.0
counter-set 85 85 68 No 71 No 85 85 68 60 No 14.0
reversed() No No 96 No No No No No 96 No No No
CSS counter scope/inheritance is compatible with HTML ordinals No No 82 No No No No No 82 No No No
Does not apply implicit counter-reset: list-item No No 68 No No No No No 68 No No No