CSS has several ways to generate counters and markers, but some use cases require more control over how those markers are displayed.

The CSS symbols() function provides a way to define a custom counter style directly in CSS. Instead of relying only on predefined numbering styles such as decimal or lower-alpha, developers can define their own sequence of symbols.

This is useful for custom lists, navigation steps, labels, and other interfaces where the marker needs a specific visual pattern.

What Is symbols()?

The symbols() function defines a custom counter style.

A simple example is:

.steps {
  list-style-type: symbols("One" "Two" "Three");
}

The list can then use the defined symbols as its markers.

For example:

<ol class="steps">
  <li>Install the application</li>
  <li>Configure the database</li>
  <li>Start the application</li>
</ol>

Instead of normal numeric markers, the list uses the custom symbol sequence.

Basic Syntax

The general syntax is:

symbols(<symbol> <symbol> ...);

For example:

.steps {
  list-style-type: symbols("Step A" "Step B" "Step C");
}

The symbols can be strings or other supported CSS symbol values.

The important point is that symbols() defines the counter style; it does not directly insert arbitrary text into the page like a pseudo-element.

A Practical Example

Consider a setup guide:

<ol class="setup">
  <li>Create a project</li>
  <li>Install dependencies</li>
  <li>Configure the application</li>
  <li>Run the project</li>
</ol>

You can create custom markers:

.setup {
  list-style-type: symbols(
    "Create"
    "Install"
    "Configure"
    "Run"
  );
}

This can make a technical checklist easier to scan when the marker itself communicates the step.

Why Use symbols()?

Without custom counter styles, developers often use additional HTML or pseudo-elements.

For example:

.step::before {
  content: "Step";
}

That approach can work, but it mixes the visual marker with the content structure.

With a counter style:

.setup {
  list-style-type: symbols("Create" "Install" "Configure" "Run");
}

the list semantics remain intact.

This is particularly useful when the content is genuinely a list and the custom marker is presentation.

symbols() With Repeating Values

A custom symbol sequence can also be used for repeated counters.

For example:

.list {
  list-style-type: symbols("A" "B");
}

For a longer list, the counter style can continue according to the defined symbol system rather than requiring developers to manually add labels to every item.

This is one reason CSS counter styles are useful for generated numbering.

Using Symbols With Ordered Lists

You can apply the style to an ordered list:

ol.steps {
  list-style-type: symbols("Step 1" "Step 2" "Step 3");
}

HTML remains straightforward:

<ol class="steps">
  <li>Prepare the environment</li>
  <li>Build the application</li>
  <li>Deploy the application</li>
</ol>

This keeps the order represented by HTML rather than hard-coding numbers into the markup.

symbols() vs ::before

These approaches solve different problems.

Feature

symbols()

::before

Intended for

Counter/list markers

Generated content

Keeps list semantics

Yes

Yes, but content is separate

Custom numbering

Yes

Manual

Useful for ordered lists

Yes

Sometimes

Requires extra markup

No

No

Best for

Custom counter styles

Decorative or contextual content

If you are creating a custom list marker, symbols() is often a cleaner CSS-based solution.

Keep the HTML Semantic

One advantage of this approach is that the HTML can remain semantic.

Prefer:

<ol class="steps">
  <li>Create the project</li>
  <li>Configure the project</li>
  <li>Run the application</li>
</ol>

and control presentation in CSS.

Avoid changing meaningful list content simply to achieve a visual numbering effect.

Browser Compatibility

When using a relatively new CSS feature, check the browser support required by your application.

For production applications, do not assume that support in one browser version means universal support.

A practical approach is:

  1. Check your target browser matrix.

  2. Test the feature in supported browsers.

  3. Provide a sensible fallback.

  4. Avoid making essential content dependent on the custom marker.

For example:

.steps {
  list-style-type: decimal;
}

@supports (list-style-type: symbols("A" "B")) {
  .steps {
    list-style-type: symbols("A" "B");
  }
}

The fallback preserves normal numbering when the custom style is unavailable.

Accessibility Considerations

Custom markers should not replace meaningful content.

For example, this is good:

<li>Install Node.js</li>

The user still receives the actual instruction even if the custom marker is not displayed.

Avoid putting essential information only inside a visual marker.

The list should remain understandable when CSS is disabled or the custom feature is unsupported.

Common Mistakes

Using symbols() for Ordinary Text

If you simply need text before an element, ::before may be more appropriate.

Hard-Coding Important Information Into Markers

Do not rely on generated markers for critical instructions.

Ignoring Browser Support

New CSS features should always be tested against the browsers your users actually use.

Replacing Semantic HTML

CSS should enhance the list rather than replacing semantic structure.

Forgetting a Fallback

A simple fallback can keep the interface usable in browsers without the required support.

Best Practices

  • Use symbols() when the problem is specifically custom counter styling.

  • Keep list content semantic.

  • Provide a fallback for unsupported browsers.

  • Test generated markers with your accessibility requirements.

  • Avoid using custom markers for information that must always be visible.

  • Keep the CSS simple when a standard counter style already meets the requirement.

Summary

CSS symbols() provides a convenient way to define custom counter styles for lists. It can be useful when standard decimal, alphabetic, or Roman numbering does not match the design.

The biggest benefit is that developers can keep semantic HTML while moving custom marker behavior into CSS.

For production use, treat symbols() as a progressive enhancement: verify browser support, provide a fallback, and make sure the list remains understandable without the custom marker.

For a simple list, standard list-style-type is usually enough. When you need a custom counter sequence, symbols() gives CSS a cleaner way to express that requirement.