CSS counters in depth

Counters are a powerful feature of CSS generated content. Through counters we can add an automatic numbering to the elements of a web document. Thanks to the :before and :after pseudo-elements, the numbering will appear before or after the actual content of an element, respectively. In this article I'll explain how to use counters by providing some useful examples.

Adding counters to elements

The automatic numbering of CSS is controlled by two properties, counter-reset and counter-increment. Counters defined by these properties are then used with the counter() and counters() functions of the content property.

The counter-reset property can contain one or more names of counters (identifiers), optionally followed by an integer. The integer sets the value that will be incremented by the counter-increment property for any occurence of the given element. The default value is 0. Negative values are allowed.

The counter-increment property is similar to the previous property. The basic difference here is that this property actually increments a counter. Its default increment is 1. Negative values are allowed.

Now we are ready to create a practical example. Given the following markup:


    <dl>
        <dt>...</dt>
        <dd>...</dd>
        <!--....-->
    </dl>
    

we want to add a progressive numbering (1, 2, 3) to each definition term (dt) in the list. The relevant CSS is the following:


dl {
    counter-reset: term;
}

dt:before {
    counter-increment: term;
    content: counter(term);
}
    

The first rule in the previous listing sets a counter for the definition list. This is called a scope. The name (an identifier) of the counter is term. Keep in mind that once we've chosen a name for our counter this must be the same also in the counter-increment property (of course se should use a meaningful name).

In the second rule we attach the :before pseudo-element to the dt element, since we want to insert the counter exactly before the actual content of the element. Now let's take a closer look at the second declaration of the second rule. The counter() function accepts our identifier (term) as its argument and the content property actually generates the counter.

As you can see, there's no space between the number and the content of the element. If we want to add more space and, say, a period (.) after the number, we could insert the following string in the content property:


dl {
    counter-reset: term;
}
    
dt:before {
    counter-increment: term;
    content: counter(term) " . ";
}
    

Note that the string inside the double quotes is treated literally, that is, the space after the period is inserted as we've typed it on the keyboard. In fact, the content property can be regarded as the CSS counterpart of the JavaScript document.write() method except that this property doesn't add real content to the document. Simply put, the content property creates a mere abstraction on the document tree but it doesn't modify it.

In case you're wondering, we can add more styles to counters by applying other properties to the attached pseudo-element. For example:


dt:before {
    counter-increment: term;
    content: counter(term);
    font-weight: bold;
    color: #999;
}
    

Now our counters are a little bit more attractive.

Furthermore, counters can be negative. When dealing with negative counters, we should only keep in mind a little math, namely the part concerning addition and subtraction of negative and positive numbers. For example, if we need a progressive numbering starting from 0, we could write the following:


dl { counter-reset: term -1; }    
    
dt:before {
    counter-increment: term;
    content: counter(term);
}
    

By setting the counter-reset property to -1 and incrementing it by 1, the resulting value is 0 and the numbering will actually start from that value. Negative counters can combine with positive counters to create interesting results. Consider the following example:


dl { counter-reset: term -1; }    
dt:before {
    counter-increment: term 3;
    content: counter(term);
}
    

As you can see, addition and substraction of negative and positive numbers yield a wide range of combination between counters. With just a simple set of calculations we can get a complete control over this automatic numbering.

Nested counters

Another interesting feature of CSS counters lies in their capability of being nested. In fact, numbering may proceed also by using progressive sublevels, such as 1.1, 1.1.1, 2.1 and so on. For example, if we want to add a sublevel to the elements of our list, we could write the following:


dl { counter-reset: term definition; }    
dt:before {
    counter-increment: term;
    content: counter(term) ". ";
}
dd:before {
    counter-increment: definition;
    content: counter(term) "." counter(definition) " ";
}
    

This example is similar to the first one, but in this case we have two counters, term and definition. The scope of both counters is set by the first rule and "lives" in the dl element. The second rule inserts the first counter before each definition term of the list. This rule is not particularly interesting, since its effect is already known. Instead, the last rule is the core of our code because it:

  1. increments the second counter (definition) on dd elements
  2. inserts the first counter (term), followed by a period
  3. inserts the second counter (definition), followed by a space.

Note that steps #2 and #3 are both performed by the content property used on the :before pseudo-element attached to the definition term.

Another interesting thing to remember is that counters are "self-nesting" in the sense that resetting a counter on a descendant element (or pseudo-element) automatically creates a new instance of the counter. This is useful in the case of (X)HTML lists, where elements may be nested with arbitrary depth. However, it's not always possible to specify a different counter for each list, since this approach might produce a really redundant code. In that vein, it's useful to mention the counters() function. This function creates a string containing all the counters having the same name of the given counter in the scope. Counters are then separated by a string. For example, given the following markup:


<ol>
  <li>...
    <ol>
      <li>...
        <ol>
          <li>...</li>
        </ol>
      </li>
    </ol>
  </li>
</ol>          

The following CSS numbers the nested list items as 1, 1.1, 1.1.1, etc.


ol { counter-reset: item; }
li { display: block; }

li:before {
  counter-increment: item;
  content: counters(item, ".") " ";
}      
    

In this example we have only the item counter for each nesting level. Instead of writing three different counters (e.g. item1, item2, item3) and thus creating three different scopes for each nested ol element, we can rely on the counters() function to achieve this goal. The second rule is really important and deserves a further explanation. Since ordered lists have a default marker (a number), we get rid of these marker by turning the list items into block-level elements. Keep in mind that only elements with display: list-items have markers. Now we can look carefully at the third rule that actually does the work. The first declaration increments the counter previously set on the outermost list. Then, in the second declaration, the counters() function creates all the counter's instances for the innermost lists. The structure of this function is as follows:

  1. its first argument is the name of the given counter, immediately followed by a comma
  2. its second argument is a period inserted in double quotes.

Note that we've inserted a space after the counters() function in order to keep the numbers separate from the actual content of the list items.

Styles of counters

Counters are formatted with decimal numbers by default. However, the styles of the list-style-type property are also available for counters. The default notation is counter(name) (no style) or counter(name, 'list-style-type') if we want to change the default formatting. In practice, the recommended styles are:

  • decimal
  • decimal-leading-zero
  • lower-roman
  • upper-roman
  • lower-greek
  • lower-latin
  • upper-latin
  • lower-alpha
  • upper-alpha

because we should always bear in mind that we're working with numeric systems. Furthermore, we should also be aware of the fact that the specifications don't define the rendering of alphabetical systems after the end of the alphabet. For example, after 26 list items the rendering of lower-latin is undefined. Real numbers are thus recommended for long lists. Here's an example:


dl { counter-reset: term definition; }

dt:before {
  counter-increment: term;
  content: counter(term, upper-latin) ". ";
} 

dd:before {
  counter-increment: definition;
  content: counter(definition, lower-latin) ". ";
}
    

We can also add styles to the counters() function, as shown in the following example.


li:before {
  counter-increment: item;
  content: counters(term, ".", lower-roman) " ";
} 
    

Note that the counters() function can also accept a third argument (lower-roman) as the last member of its arguments list, separated by a second comma from the preceding period. However, the counters() function doesn't allow us to specify different styles for each level of nesting.

Back to top