CSS counters() Function

The CSS counters() function combines values from nested CSS counters with the same name into a single formatted string.

It is commonly used to create hierarchical numbering such as 1, 1.1, and 1.1.1 for nested lists, document outlines, and multi-level sections. The function uses a separator to join counter values and automatically updates the numbering when the document structure changes.

Syntax

counters(name, separator)
counters(name, separator, style)

The function requires a counter name and a separator string. An optional third parameter controls the numbering style.

For example:

content: counters(item, ".");

This combines the values of nested counters named item, separating each level with a period.

Additional examples:

content: counters(item, ".");
content: counters(item, ".") ". ";
content: counters(section, "-");
content: counters(chapter, " / ");
content: counters(item, ".", decimal);
content: counters(item, ".", upper-roman);
content: counters(item, "-", lower-alpha);

The separator must be a quoted CSS string. It is inserted between counter values, not automatically before or after the complete result.

The function returns formatted text for use in generated content.

Parameters

The counters() function accepts two required parameters and one optional parameter.

Parameter Description
name The case-sensitive name of the CSS counter whose nested values should be displayed.
separator A CSS string inserted between the formatted values of the nested counter instances.
style An optional counter style, such as decimal, lower-alpha, or upper-roman. The default is decimal.

For example:

counters(item, ".")
counters(item, "-", upper-roman)

For counter values 2, 3, and 1, the first expression produces 2.3.1, while the second produces II-III-I.

The style applies to every counter value included in the result.

A counter style can also be defined using @counter-style.

How It Works

The counters() function reads the active counter instances with the specified name, from the outermost scope to the innermost scope.

Each counter value is formatted according to the selected style, and the values are joined using the separator.

For example, consider a nested list:

1. HTML
   1.1 Elements
   1.2 Attributes
2. CSS
   2.1 Selectors
   2.2 Properties

CSS can generate this numbering automatically:

.outline ol {
  counter-reset: item;
  list-style: none;
}
.outline li {
  counter-increment: item;
}
.outline li::before {
  content: counters(item, ".") ". ";
}

Each nested <ol> creates a new counter instance named item.

When the function is evaluated, it combines the current values of the relevant nested counter instances.

Adding or removing list items automatically updates the generated numbering.

Counter Scopes

CSS counters use scopes to determine which counter values are available at a particular point in the document.

A nested list can establish a new counter with the same name while retaining access to the outer counter value.

For example:

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

At the first level, the function may return 1.

Inside a nested list, it may return 1.1.

Inside another nested list, it may return 1.1.1.

The same counter name is used at every level, but each nested scope maintains its own value.

If only one counter instance is available, counters() returns that single formatted value without inserting a separator.

When no counter with the requested name exists, the function behaves as though a counter with a value of zero were available.

Separators

The separator controls the characters inserted between the counter values.

For example:

counters(item, ".")
counters(item, "-")
counters(item, " / ")
counters(item, ":")
counters(item, " ")

For counter values 1, 2, and 3, these produce:

Separator Result
"."1.2.3
"-"1-2-3
" / "1 / 2 / 3
":"1:2:3
" "1 2 3

An empty separator is also valid:

content: counters(item, "");

This joins the formatted values without adding characters between them.

To add punctuation after the complete numbering sequence, include another string in the content declaration:

content: counters(item, ".") ". ";

For a nested counter value of 2.1, the displayed prefix becomes 2.1. followed by a space.

Counter Styles

The optional third parameter controls how each counter value is represented.

Style Example for Values 2 and 3
decimal2.3
decimal-leading-zero02.03
lower-alphab.c
upper-alphaB.C
lower-romanii.iii
upper-romanII.III

For example:

.outline li::before {
  content: counters(item, ".", upper-roman) ". ";
}

Nested values such as 2 and 3 are displayed as II.III..

Custom styles can be defined with @counter-style:

@counter-style section-letters {
  system: alphabetic;
  symbols: A B C D E F;
  suffix: ". ";
}
.outline li::before {
  content: counters(item, "-", section-letters) " ";
}

The custom style formats each counter value, while the separator joins the values.

The suffix defined by @counter-style is not automatically included when the style is used inside counters().

Counter Properties

The counters() function works with the same counter management properties as counter().

Property Purpose
counter-reset Creates or resets a counter, allowing nested scopes to establish separate counter instances.
counter-increment Increases or decreases the current counter value.
counter-set Assigns a specific value to a counter.
content Displays the combined counter values in generated content.

For example:

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

Each nested list creates a new counter instance, each list item increments its current counter, and the function displays the hierarchy.

The counters() function itself does not create or change counter values.

Nested Lists

Nested ordered lists are a common use for counters().

For example:

<div class="outline">
  <ol>
    <li>HTML
      <ol>
        <li>Elements</li>
        <li>Attributes</li>
      </ol>
    </li>
    <li>CSS
      <ol>
        <li>Selectors</li>
        <li>Properties</li>
      </ol>
    </li>
  </ol>
</div>

The CSS:

.outline ol {
  counter-reset: item;
  list-style: none;
}
.outline li {
  counter-increment: item;
}
.outline li::before {
  content: counters(item, ".") ". ";
}

The generated numbering is:

1. HTML
   1.1. Elements
   1.2. Attributes
2. CSS
   2.1. Selectors
   2.2. Properties

The nested <ol> elements establish separate counter instances, while the <li> elements increment their current values.

Using the same counter name at each level allows counters() to combine the hierarchy automatically.

Document Numbering

The counters() function can also generate multi-level numbering for document sections.

For example:

.document {
  counter-reset: section;
}
.document .section {
  counter-increment: section;
}
.document .subsections {
  counter-reset: section;
}
.document .section-title::before {
  content: counters(section, ".") " ";
}

When nested section containers establish additional counters with the same name, the numbering can represent multiple levels:

1 Introduction
1.1 Overview
1.2 Requirements
2 CSS Layout
2.1 Flexbox
2.2 Grid

This technique is useful for documents whose headings follow a nested section structure.

For headings arranged as siblings rather than nested containers, separate named counters with counter() may be easier to manage.

Choose the approach that matches the actual HTML structure.

Example

This example demonstrates hierarchical numbering for a nested list.

HTML:

<div class="counters-demo">
  <ol>
    <li>HTML
      <ol>
        <li>Elements</li>
        <li>Attributes</li>
      </ol>
    </li>
    <li>CSS
      <ol>
        <li>Selectors</li>
        <li>Properties</li>
      </ol>
    </li>
  </ol>
</div>

CSS:

.counters-demo ol {
  counter-reset: demo-item;
  list-style: none;
  padding-left: 25px;
}
.counters-demo li {
  counter-increment: demo-item;
  margin: 8px 0;
}
.counters-demo li::before {
  content: counters(demo-item, ".") ". ";
  color: #0066cc;
  font-weight: bold;
}
Browser Result
  1. HTML
    1. Elements
    2. Attributes
  2. CSS
    1. Selectors
    2. Properties

The outer list displays 1 and 2. Each nested list adds another level, producing values such as 1.1, 1.2, 2.1, and 2.2.

Adding another nested level would extend the numbering to values such as 1.1.1.

Counter Style Example

This example uses uppercase Roman numerals for hierarchical numbering.

HTML:

<div class="counters-roman-demo">
  <ol>
    <li>Getting Started
      <ol>
        <li>HTML Syntax</li>
        <li>CSS Syntax</li>
      </ol>
    </li>
    <li>Layout
      <ol>
        <li>Flexbox</li>
        <li>Grid</li>
      </ol>
    </li>
  </ol>
</div>

CSS:

.counters-roman-demo ol {
  counter-reset: roman-item;
  list-style: none;
  padding-left: 25px;
}
.counters-roman-demo li {
  counter-increment: roman-item;
  margin: 8px 0;
}
.counters-roman-demo li::before {
  content: counters(roman-item, ".", upper-roman) ". ";
  color: #0066cc;
  font-weight: bold;
}
Browser Result
  1. Getting Started
    1. HTML Syntax
    2. CSS Syntax
  2. Layout
    1. Flexbox
    2. Grid

The generated numbering uses I, I.I, I.II, II, II.I, and II.II.

The same counter structure can use decimal, alphabetic, or custom numbering styles by changing the third parameter.

counters() vs. counter()

The counters() and counter() functions both display counter values, but they differ in how they handle nested counter scopes.

Feature counter() counters()
Purpose Displays one counter value. Displays a hierarchy of counter values.
Required arguments Counter name. Counter name and separator.
Optional argument Counter style. Counter style.
Nested scopes Uses the innermost matching counter. Combines matching counter instances from outermost to innermost.
Example counter(item) counters(item, ".")

For a nested counter hierarchy with values 2, 3, and 1:

counter(item)       /* 1 */
counters(item, ".") /* 2.3.1 */

Use counter() when only the current value is needed.

Use counters() when the complete nested hierarchy should be displayed.

Accessibility

Hierarchical numbering can make a document easier to navigate visually, but CSS-generated numbers should not replace semantic HTML structure.

For nested lists, use actual <ol> and <li> elements:

<ol>
  <li>HTML
    <ol>
      <li>Elements</li>
      <li>Attributes</li>
    </ol>
  </li>
</ol>

These elements preserve list relationships for browsers and assistive technologies even when CSS is unavailable.

CSS-generated numbering may be announced differently across screen readers and browsers.

If a specific number is essential for understanding instructions or cross-references, ensure that the information remains accessible without relying exclusively on generated content.

For document headings, retain meaningful heading levels such as <h2> and <h3>.

Common Uses

The counters() function is useful for hierarchical numbering systems.

Use Description
Nested ordered lists Display numbering such as 1, 1.1, and 1.1.1.
Document outlines Number nested sections and subsections automatically.
Technical documentation Create hierarchical numbering for structured reference material.
Multi-level instructions Number steps and substeps within a process.
Custom numbering formats Combine hierarchical values with decimal, alphabetic, or Roman numeral styles.
Dynamic content Update numbering automatically when nested elements are added or removed.

For example, a nested outline can use:

.outline ol {
  counter-reset: section;
}
.outline li {
  counter-increment: section;
}
.outline li::before {
  content: counters(section, ".") " ";
}

This generates hierarchical section numbers from the HTML nesting structure.

Limitations

The counters() function has several important rules and limitations.

  • It requires both a counter name and a separator string.
  • The optional third argument must identify a valid counter style.
  • It combines counter instances with the same name rather than unrelated named counters.
  • It depends on CSS counter scopes and the document structure.
  • It does not create, increment, or reset counters.
  • It returns formatted text, not a numeric value for calculations.
  • When only one matching counter instance is available, no separator is inserted.
  • Generated numbering does not automatically create semantic HTML list relationships.

For example, this is valid:

content: counters(item, ".");

The following is invalid because the separator is missing:

content: counters(item);

The following is also invalid because the separator is not a CSS string:

content: counters(item, .);

Always provide a quoted separator, even when it is an empty string.

Browser Support

The CSS counters() function is Baseline Widely available and has been supported across major browsers for many years.

It works in current versions of Chrome, Edge, Firefox, Safari, and other modern browsers.

For a fallback, use semantic nested ordered lists so the document retains its structure when custom CSS numbering is unavailable.

For example:

<ol>
  <li>First section
    <ol>
      <li>First subsection</li>
      <li>Second subsection</li>
    </ol>
  </li>
</ol>

Native list markers remain available when custom counter styling is not applied.

For current compatibility information, visit Can I Use? .

Specifications

The counters() function is defined in the CSS Lists and Counters Module Level 3 .

The specification defines how counter values are combined, how separators are inserted, and how counter styles are applied.

Related CSS features include counter(), symbols(), counter-reset, counter-increment, counter-set, and @counter-style.