CSS@counter-style

@counter-style: Custom List Numbering

Built-in list markers stop at decimal, disc, lower-roman, and a handful of friends. @counter-style lets you invent your own numbering system — emoji bullets, bracketed numbers, custom alphabets, even weighted systems like roman numerals — and use it anywhere a list-style-type or counter() is accepted.

First Example

CSS
@counter-style thumbs {
  system: cyclic;
  symbols: "👍";
  suffix: " ";
}

ul.likes {
  list-style: thumbs;
}
👍 First item
👍 Second item
👍 Third item
The Descriptors

Descriptor

Purpose

Default

system

Algorithm that turns a number into a marker

symbolic

symbols

The symbols the system draws from

(required for most systems)

additive-symbols

Weighted symbol list for the additive system

prefix

String placed before every marker

"" (empty)

suffix

String placed after every marker

". "

range

Counter values this style covers

infinite

pad

Minimum length, padded with a symbol

0 ""

negative

How negative values are rendered

"-"

fallback

Style used outside range or on failure

decimal

The system Descriptor

system chooses the algorithm. The four you will actually use:

CSS
/* cyclic — repeat the symbols forever: a, b, c, a, b, c ... */
@counter-style stars {
  system: cyclic;
  symbols: "★" "☆";
  suffix: " ";
}

/* numeric — positional number system; symbols are the digits.
   Two symbols = binary, ten = decimal-like */
@counter-style binary {
  system: numeric;
  symbols: "0" "1";
}
/* 1 → 1, 2 → 10, 3 → 11, 4 → 100 */

/* alphabetic — like spreadsheet columns: a..z, aa, ab ... */
@counter-style greek-letters {
  system: alphabetic;
  symbols: "α" "β" "γ" "δ" "ε";
  suffix: ") ";
}
/* 1 → α, 5 → ε, 6 → αα */

/* fixed — each symbol used exactly once, then fallback */
@counter-style podium {
  system: fixed;
  symbols: "🥇" "🥈" "🥉";
  suffix: " ";
  fallback: decimal;
}
/* 1 → 🥇, 2 → 🥈, 3 → 🥉, 4 → 4. */
The additive System

additive builds the marker by summing weighted symbols, largest first — exactly how roman numerals work. Weights must be listed in descending order:

CSS
@counter-style simple-roman {
  system: additive;
  additive-symbols:
    1000 "M", 900 "CM", 500 "D", 400 "CD",
    100 "C", 90 "XC", 50 "L", 40 "XL",
    10 "X", 9 "IX", 5 "V", 4 "IV", 1 "I";
  suffix: ". ";
}
/* 1984 → MCMLXXXIV. */

/* Tally marks */
@counter-style tally {
  system: additive;
  additive-symbols: 5 "𝍸", 1 "𝍷";
  suffix: " ";
}
/* 7 → 𝍸𝍷𝍷 */
prefix, suffix, pad, and range

CSS
/* Legal-style bracketed numbers: [01] [02] ... */
@counter-style bracketed {
  system: numeric;
  symbols: "0" "1" "2" "3" "4" "5" "6" "7" "8" "9";
  prefix: "[";
  suffix: "] ";
  pad: 2 "0";
}

/* Only style the first ten items, then fall back */
@counter-style first-ten {
  system: cyclic;
  symbols: "▸";
  range: 1 10;
  fallback: decimal;
  suffix: " ";
}
fallback is your safety net
Whenever a value is outside range, or afixed system runs out of symbols, the browser renders thefallback style instead (default: decimal). Always pick a sensible fallback for finite systems.
Using Custom Styles with counter()

Custom counter styles are not just for list-style-type. The counter() and counters() functions accept a style name as their second argument, which is how you number arbitrary elements — headings, figures, steps:

CSS
@counter-style circled {
  system: fixed;
  symbols: "①" "②" "③" "④" "⑤" "⑥" "⑦" "⑧" "⑨" "⑩";
  suffix: " ";
}

.steps {
  counter-reset: step;
  list-style: none;
}

.steps li {
  counter-increment: step;
}

.steps li::before {
  content: counter(step, circled);
  margin-inline-end: 0.5em;
}

counters() (plural) joins nested counter values with a separator — ideal for outline numbering like 1.2.3:

CSS
ol.outline {
  counter-reset: item;
  list-style: none;
}
ol.outline li {
  counter-increment: item;
}
ol.outline li::before {
  /* "1", "1.1", "1.1.1" ... */
  content: counters(item, ".") " ";
  font-weight: 600;
}
Inline One-Off: symbols()

For a quick style you use once, the symbols() function skips the at-rule entirely (cyclic, numeric, alphabetic, and fixed only):

CSS
ul.dashes {
  list-style: symbols(cyclic "—");
}

ol.letters {
  list-style: symbols(alphabetic "a" "b" "c" "d");
}
Note
symbols() has narrower browser support than@counter-style itself — check support before shipping. The at-rule form also gives you prefix,suffix, pad, and range, which the function cannot express.
Real-World Recipe: Styled FAQ Numbers

CSS
@counter-style q-style {
  system: numeric;
  symbols: "0" "1" "2" "3" "4" "5" "6" "7" "8" "9";
  prefix: "Q";
  suffix: ". ";
}

.faq {
  counter-reset: question;
}

.faq h3 {
  counter-increment: question;
}

.faq h3::before {
  content: counter(question, q-style);
  color: teal;
  font-variant-numeric: tabular-nums;
}
/* Renders: Q1. Q2. Q3. ... */
Key Takeaways
  • system picks the algorithm: cyclic repeats, fixed runs once, numeric is positional, alphabetic is spreadsheet-style, additive sums weights (roman numerals).

  • symbols feeds most systems; additive-symbols (descending weights) feeds additive.

  • prefix/suffix wrap the marker; pad zero-fills; range limits coverage with fallback as the safety net.

  • Use custom styles anywhere: list-style-type, counter(), and counters() all accept the style name.

  • symbols() is the inline shortcut for one-offs, with more limited support and features.

Tip
Keep markers accessible: screen readers generally announce list semantics, not your decorative symbols. Purely decorative markers are fine, but never encode essential meaning only in the marker.