CSS counter-set: Assigning Custom Counter Values

The CSS counter-set property provides a direct way to modify the value of an active CSS counter without altering its nesting or scoping structure. While CSS counters typically rely on automatic step-by-step additions, counter-set allows developers to assign any explicit, arbitrary integer to a counter sequence mid-flow. This guide explains how counter-set operates, how it interacts with existing scopes, and how to use it to inject custom values into your document numbering.

Understanding counter-set

CSS counters work using three core properties: counter-reset to initialize a counter, counter-increment to step through values, and counter-set to force a counter to a specific integer.

The syntax for the property is straightforward:

counter-set: <counter-name> <integer>?;
  • <counter-name>: The name of the counter you want to alter.
  • <integer>: The target number to assign to the counter. This value can be positive, negative, or zero. If omitted, it defaults to 0.

Multiple counters can be manipulated simultaneously within a single declaration by separating pairs with spaces:

counter-set: section-count 10 page-count 1;

Modifying Counters Without Breaking Scope

The key distinction of counter-set lies in how it handles counter scoping compared to counter-reset:

  • counter-reset creates a new instance of the counter on the target element. If applied to a child element inside an existing list, it instantiates a nested counter, leaving the parent counter unchanged.
  • counter-set seeks the existing instance of the counter in the current scope and overrides its current value. It does not spawn a new counter scope.

This behavior makes counter-set the appropriate choice when you need to jump to a specific number or correct an existing sequence without unintentionally creating hierarchical sub-lists.

Practical Implementation

Consider a scenario where an ordered list requires a non-standard jump in numbering, such as skipping from item 2 directly to item 50:

<ol class="custom-list">
  <li>First item</li>
  <li>Second item</li>
  <li class="skip-ahead">Intermission or special entry</li>
  <li>Subsequent item</li>
</ol>
.custom-list {
  list-style: none;
  counter-reset: custom-counter 0;
}

.custom-list li {
  counter-increment: custom-counter;
}

.custom-list li::before {
  content: counter(custom-counter) ". ";
}

/* Assign an arbitrary number */
.custom-list li.skip-ahead {
  counter-set: custom-counter 50;
}

In this example:

  1. The first item increments the counter to 1.
  2. The second item increments it to 2.
  3. The third item matches .skip-ahead, so counter-set forces custom-counter to 50. (Because counter-increment also applies here, order of evaluation places counter-set after counter-increment when on the same element, ensuring the rendered output reflects 50).
  4. The fourth item increments normally from the new baseline, rendering as 51.

Order of Operations on the Same Element

When multiple counter properties appear on the same element, the browser applies them in a strict sequence:

  1. counter-reset: Generates new counter scopes.
  2. counter-increment: Adjusts existing or newly generated counters by their specified offsets.
  3. counter-set: Overwrites the counter's value with the specified integer.

Because counter-set evaluates last, any arbitrary value assigned via counter-set overrides any increment applied by counter-increment on that identical element. Subsequent elements then continue incrementing or calculating based on the newly assigned number.