@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
@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:
/* 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:
@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
/* 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: " ";
}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:
@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:
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):
ul.dashes {
list-style: symbols(cyclic "—");
}
ol.letters {
list-style: symbols(alphabetic "a" "b" "c" "d");
}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
@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.