Counters are a powerful feature of 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>term</dt> <dd>definition</dd> <dt>term</dt> <dd>definition</dd> <dt>term</dt> <dd>definition</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:
dt:before { 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 { content: counter(term); padding: 1px 2px; margin-right: 0.2em; background: #ffc; color: #000; border: 1px solid #999; font-weight: bold; }
We've just set a background color, added some padding and a right margin, made the font bold and outlined the counters with a thin solid border. Now our counters are a little more attractive.
Furthermore, counters can be negative. When dealing with negative counters, we should only keep in mind a little maths, 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:
- increments the second counter (
definition
) ondd
elements - inserts the first counter (
term
), followed by a period - 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>item</li> <li>item <ol> <li>item</li> <li>item</li> <li>item <ol> <li>item</li> <li>item</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:
- its first argument is the name of the given counter, immediately followed by a comma
- 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 at 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(item, ".", 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.