CSS @counter-style: Custom List Bullet Markers

The CSS @counter-style at-rule allows developers to define custom counter styles for ordered and unordered lists, moving beyond the default browser markers like numbers, discs, and roman numerals. This article provides a straightforward guide to understanding how @counter-style works, its core descriptor properties, and how to implement it to create clean, reusable custom bullets and numbering systems.


What Is the @counter-style At-Rule?

Traditionally, styling list markers with custom characters, emojis, or specialized counting systems required replacing the marker entirely using ::before pseudo-elements. The @counter-style at-rule solves this by allowing you to define a named counter algorithm directly in your stylesheet and apply it anywhere using the standard list-style-type property or CSS counter functions.

Key Descriptors in @counter-style

To build a custom marker system, @counter-style uses several core descriptors:

  • system: Defines the logic used to generate markers from the counter value. Common values include:
    • cyclic: Cycles through the provided symbols repeatedly (ideal for bullet markers).
    • numeric: Treats symbols as digits in an positional numbering system.
    • alphabetic: Interprets symbols like an alphabet (e.g., A-Z, AA-ZZ).
    • fixed: Applies symbols only to a limited, specified range before stopping.
    • symbolic: Repeatedly prints a symbol corresponding to the counter integer (e.g., *, **, ***).
  • symbols: Specifies the space-separated list of characters, strings, or emojis used as the marker symbols.
  • suffix: Defines the content appended directly after the marker symbol (defaults to ". " in most cases).
  • prefix: Defines content prepended directly before the marker symbol.
  • pad: Defines minimum character lengths and zero-padding (e.g., pad: 2 "0" creates 01, 02, 03).
  • fallback: Designates a backup system (such as decimal or disc) if the custom counter exceeds its defined range or fails to render.
  • speak-as: Informs screen readers how to pronounce the custom markers (e.g., bullets, numbers, or words).

Practical Examples

1. Custom Emoji Bullet List

Using the cyclic system, you can cycle through a custom set of bullet markers for an unordered list:

@counter-style task-markers {
  system: cyclic;
  symbols: "👉" "⭐" "✅";
  suffix: " ";
}

ul.custom-bullets {
  list-style-type: task-markers;
}

2. Padded Numbered List

Using the numeric system alongside the pad descriptor, you can create zero-padded ordered lists:

@counter-style zero-padded {
  system: numeric;
  symbols: "0" "1" "2" "3" "4" "5" "6" "7" "8" "9";
  pad: 2 "0";
  suffix: ") ";
}

ol.padded-list {
  list-style-type: zero-padded;
}

This generates items formatted as 01), 02), 03), etc.


Advantages of Using @counter-style

  • Semantic Cleanliness: Keeps list formatting inside native list properties (list-style-type) rather than relying on pseudo-element workarounds.
  • Reusability: Once declared, the counter style can be reused across any element, including CSS counters() and ::marker pseudo-elements.
  • Accessibility: The speak-as descriptor ensures that visual embellishments do not degrade the experience for users relying on screen readers.