Yes, you are reading that correctly: This is indeed a guide to styling counters with CSS. Some of you are cheering, “Finally!”, but I understand that the vast majority of you are thinking, “Um, it’s just styling lists.” If you are part of the second group, I get it. Before learning and writing more and more about counters, I thought the same thing. Now I am part of the first group, and by the end of this guide, I hope you join me there.

There are many ways to create and style counters, which is why I wanted to write this guide and also how I plan to organize it: going from the most basic styling to the top-notch level of customization, sprinkling in between some sections about spacing and accessibility. It isn’t necessary to read the guide in order — each section should stand by itself, so feel free to jump to any part and start reading.

Customizing Counters in HTML

Lists elements were among the first 18 tags that made up HTML. Their representation wasn’t defined yet but deemed fitting a bulleted list for unordered lists, and a sequence of numbered paragraphs for an ordered list.

Cool but not enough; soon people needed more from HTML alone and new list attributes were added throughout the years to fill in the gaps.

start

The start attribute takes an integer and sets from where the list should start:

<ol start="2">
  <li>Bread</li>
  <li>Milk</li>
  <li>Butter</li>
  <li>Apples</li>
</ol>
  1. Bread
  2. Milk
  3. Butter
  4. Apples

Although, it isn’t limited to positive values; zero and negative integers are allowed as well:

<ol start="0">
  <li>Bread</li>
  <li>Milk</li>
  <li>Butter</li>
  <li>Apples</li>
</ol>

<ol start="-2">
  <li>Bread</li>
  <li>Milk</li>
  <li>Butter</li>
  <li>Apples</li>
</ol>
  1. Bread
  2. Milk
  3. Butter
  4. Apples
  1. Bread
  2. Milk
  3. Butter
  4. Apples

type

We can use the type attribute to change the counter’s representation. It’s similar to CSS’s list-style-type, but it has its own limited uses and shouldn’t be used interchangeably*. Its possible values are:

  • 1 for decimal numbers (default)
  • a for lowercase alphabetic
  • A for uppercase alphabetic
  • i for lowercase Roman numbers
  • I for uppercase Roman numbers
<ol type="a">
  <li>Bread</li>
  <li>Milk</li>
  <li>Butter</li>
  <li>Apples</li>
</ol>

<ol type="i">
  <li>Bread</li>
  <li>Milk</li>
  <li>Butter</li>
  <li>Apples</li>
</ol>
  1. Bread
  2. Milk
  3. Butter
  4. Apples
  1. Bread
  2. Milk
  3. Butter
  4. Apples

It’s weird enough to use type on ol elements, but it still has some use cases*. However, usage with the ul element is downright deprecated.

value

The value attribute sets the value for a specific li element. This also affects the values of the li elements after it.

<ol>
  <li>Bread</li>
  <li value="4">Milk</li>
  <li>Butter</li>
  <li>Apples</li>
</ol>
  1. Bread
  2. Milk
  3. Butter
  4. Apples

reversed

The reversed attribute will start counting elements in reverse order, so from highest to lowest.

<ol reversed>
  <li>Bread</li>
  <li>Milk</li>
  <li>Butter</li>
  <li>Apples</li>
</ol>
  1. Bread
  2. Milk
  3. Butter
  4. Apples

All can be combined

If you ever feel the need, all list attributes can be combined in one (ordered) list.

<ol reversed start="2" type="i">
  <li>Bread</li>
  <li value="4">Milk</li>
  <li>Butter</li>
  <li>Apples</li>
</ol>
  1. Bread
  2. Milk
  3. Butter
  4. Apples

* Do we need them if we now have CSS?

Funny enough, the first CSS specification already included list-style-type and other properties to style lists, and it was released before HTML 3.2 — the first HTML spec that included some of the previous list attributes. This means that at least on paper, we had CSS list styling before HTML list attributes, so the answer isn’t as simple as “they were there before CSS.”

Without CSS, a static page (such as this guide) won’t be pretty, but at the very least, it should be readable. For example, the type attribute ensures that styled ordered lists won’t lose their meaning if CSS is missing, which is especially useful in legal or technical documents. Some attributes wouldn’t have a CSS equivalent until years later, including reversedstart and value.

Styling Simple Counters in CSS

For most use cases, styling lists in CSS doesn’t take more than a couple of rules, but even in that brevity, we can find different ways to style the same list.

::marker or ::before?

The ::marker pseudo-element represents the counter part of a list item. As a pseudo-element, we can set its content property to any string to change its counter representation:

li::marker {
  content: "💜 ";
}
  • Bread
  • Milk
  • Butter
  • Apples

The content in pseudo-elements also accepts images, which allows us to create custom markers:

li::marker {
  content: url("./logo.svg") " ";
}
  • bread
  • milk
  • butter
  • apples

By default, only li elements have a ::marker but we can give it to any element by setting its display property to list-item:

h4 {
  display: list-item;
}

h4::marker {
  content: "◦ ";
}

This will give each h4 a ::marker which we can change to any string:

List Title

However, ::marker is an odd case: it was described in the CSS spec more than 20 years ago, but only gained somewhat reliable support in 2020 and still isn’t fully supported in Safari. What’s worst, only font-related properties (such as font-size or color) are allowed, so we can’t change its margin or background-color.

This has led many to use ::before instead of ::marker, so you’ll see a lot of CSS in which the author got rid of the ::marker using list-style-type: none and used ::before instead:

li {
  /* removes ::marker */
  list-style-type: none;
}

li::before {
  /* mimics ::marker */
  content: "▸ ";
}

list-style-type

The list-style-type property can be used to replace the ::marker‘s string. Unlike ::markerlist-style-type has been around forever and is most people’s go-to option for styling lists. It can take a lot of different counter styles that are built-in in browsers, but you will probably use one of the following:

For unordered lists:

  • disc
  • circle
  • square
ul {
  list-style-type: square;
}

ul {
  list-style-type: circle;
}
  • bread
  • milk
  • butter
  • apples

For ordered lists:

  • decimal
  • decimal-leading-zero
  • lower-roman
  • upper-roman
  • lower-alpha
  • upper-alpha
ol {
  list-style-type: upper-roman;
}

ol {
  list-style-type: lower-alpha;
}
  1. bread
  2. milk
  • butter
  • apples

You can find a full list of valid counter styles here.

It can also take none to remove the marker altogether, and since not long ago, it can also take a <string> for ul elements.

ul {
  list-style-type: none;
}

ul {
  list-style-type: "➡️ ";
}
Two lists with two items each. The first list has no markers the second list has an emoji of a right arrow for the marker.

Creating Custom Counters

For a long time, there wasn’t a CSS-equivalent to the HTML reversestart or value attributes. So if we wanted to reverse or change the start of multiple lists, instead of a CSS class to rule them all, we had to change their HTML one by one. You can imagine how repetitive that would get.

Besides, list attributes simply had their limitations: we can’t change how they increment with each item and there isn’t an easy way to attach a prefix or suffix to the counter. And maybe the biggest reason of all is that there wasn’t a way to number things that weren’t lists!

Custom counters let us number any collection of elements with a whole new level of customization. The workflow is to:

  1. Initiate the counter with the counter-reset property.
  2. Increment the counter with the counter-increment property.
  3. Individually set the counters with the counter-set property.
  4. Output the counters with either the counter() and counters() functions.

As I mentioned, we can make a list out of any collection of elements, and while this has its accessibility concerns, just for demonstration’s sake, let’s try to turn a collection of headings like this…

<div class="index">
  <h2>The Old Buccaneer</h2>
  <h2>The Sea Cook</h2>
  <h2>My Shore Adventure</h2>
  <h2>The Log Cabin</h2>
  <h2>My Sea Adventure</h2>
  <h2>Captain Silver</h2>
</div>

…into something that looks list-like. But just because we can make an element look like a list doesn’t always mean we should do it. Be sure to consider how the list will be announced by assistive technologies, like screen readers, and see the Accessibility section for more information.

Initiate counters: counter-reset

The counter-reset property takes two things: the name of the counter as a custom ident and the initial count as an integer. If the initial count isn’t given, then it will start at 0 by default:

.index {
  counter-reset: index;
  /* The same as */
  counter-reset: index 0;
}

You can initiate several counters at once with a space-separated list and set a specific value for each one:

.index {
  counter-reset: index another-counter 2;
}

This will start our index counter at 0 (the default) and another-counter at 2.

Set counters: counter-set

The counter-set works similar to counter-reset: it takes the counter’s name followed by an integer, but this time it will set the count for that element onwards. If the integer is omitted, it will set the counter to 0 by default.

h2:nth-child(2) {
  counter-set: index;
  /* same as */
  counter-set: index 0;
}

And we can set several counters at once, as well:

h2:nth-child(3) {
  counter-set: index 5 another-counter 10;
}

This will set the third h2 element’s index count to 5 and another-counter to 10.

If there isn’t an active counter with that name, counter-set will initiate it at 0.

Increment counters: counter-increment

Right now, we have our counter, but it will stagnate at 0 since we haven’t set which elements should increment it. We can use the counter-increment property for that, which takes the name of the counter and how much it should be incremented by. If we only write the counter’s name, it will increment it by 1.

In this case, we want each h2 title to increment the counter by one, and that should be as easy as setting counter-increment to the counter’s name:

h2 {
  counter-increment: index;
  /* same as */
  counter-increment: index 1;
}

Just like with counter-reset, we can increment several counters at once in a space-separated list:

h2 {
  counter-increment: index another-counter 2;
}

This will increment index by one and another-counter by two on each h2 element.

If there isn’t an active counter with that name, counter-increment will initiate it at 0.

Output simple lists: counter()

So far, we won’t see any change in the counter representation. The counters are counting but not showing, so to output the counter’s result we use the counter() and counters() functions. Yes, those are two functions with similar names but important differences.

The counter() function takes the name of a counter and outputs its content as a string. If many active counters have the same name, it will select the one that is defined closest to the element, so we can only output one counter at a time.

As mentioned earlier, we can set an element’s display to list-item to work with its ::marker pseudo-element:

h2 {
  display: list-item;
}

Then, we can use counter() in its content property to output the current count. This allows us to prefix and suffix the counter by writing a string before or after the counter() function:

h2::marker {
  content: "Part " counter(index) ": ";
}
List of six chapters formatted as Part 1, Part 2, et cetera.

Alternatively, we can use the everyday ::before pseudo-element to the same effect:

h2::before {
  content: "Part " counter(index) ": ";
}

Output nested lists: counters()

counter() works great for most situations, but what if we wanted to do a nested list like this:

1. Paradise Beaches
   1.1. Hawaiian Islands
   1.2. Caribbean Getaway
        1.2.1. Aruba
        1.2.2. Barbados
2. Outdoor Escapades
   2.1 National Park Hike
   2.2. Mountain Skiing Trip

We would need to initiate individual counters and write different counter() functions for each level of nesting, and that’s only possible if we know how deep the nesting goes, which we simply don’t at times.

In this case, we use the counters() function, which also takes the name of a counter as an argument but instead of just outputting its content, it will join all active counters with that name into a single string and output it. To do so, it takes a string as a second argument, usually something like a dot (".") or dash ("-") that will be used between counters to join them.

We can use counter-reset and counter-increment to initiate a counter for each ol element, while each li will increment its closest counter by 1:

ol {
  counter-reset: item;
}

li {
  counter-increment: item;
}

But this time, instead of using counter() (which would only display one counter per item), we will use counters() to join all active counters by a string (e.g. ".“) and output them at once:

li::marker {
  content: counters(item, ".") ". ";
}
List of outdoor travel destinations containing nested lists formatted with decimal markers.

Styling Counters

Both the counter() and counters() functions accept one additional, yet optional, last argument representing the counter style, the same ones we use in the list-style-type property. So in our last two examples, we could change the counter styles to Roman numbers and alphabetic letters, respectively:

h2::marker {
  content: "Part " counter(index, upper-roman) ": ";
}
List of chapter titles with the marker saying Part 1, Part 2, et cetera, formatted as Roman numerals.
li::marker {
  content: counters(item, ".", lower-alpha) ". ";
}
List of outdoor travel destinations containing nested lists formatted with lowercase letters.

Reverse Counters

It’s possible to count backward using custom counters, but we need to know beforehand the number of elements we’ll count. So for example, if we want to make a Top Five list in reversed order:

<h1>Best rated animation movies</h1>

<ol>
  <li>Toy Story 2</li>
  <li>Toy Story 1</li>
  <li>Finding Nemo</li>
  <li>How to Train your Dragon</li>
  <li>Inside Out</li>
</ol>

We have to initiate our counter at the total number of elements plus one (so it doesn’t end at 0):

ol {
  counter-reset: movies 6;
}

And then set the increment to a negative integer:

li {
  counter-increment: movies -1;
}

To output the count we use counter() as we did before:

li::marker {
  content: counter(movies) ". ";
}
List of five movies with the markers displayed in reverse order.

There is also a way to write reversed counters supported in Firefox, but it hasn’t shipped to any other browser. Using the reversed() functional notation, we can wrap the counter name while initiating it to say it should be reversed.

ol {
  counter-reset: reversed(movies);
}

li {
  counter-increment: movies;
}

li::marker {
  content: counter(movies) " .";
}

Styling Custom Counters

The last section was all about custom counters: we changed from where they started and how they increased, but at the end of the day, their output was styled in one of the browser’s built-in counter styles, usually decimal. Now using @counter-style, we’ll build our own counter styles to style any list.

The @counter-style at-rule, as its name implies, lets you create custom counter styles. After writing the at-rule it takes a custom ident as a name:

@counter-style my-counter-style {
  /* etc. */
}

That name can be used inside the properties and functions that take a counter style, such as list-style-type or the last argument in counter() and counters():

ul {
  list-style-type: my-counter-style;
}

li::marker {
  content: counter(my-counter, my-counter-style) ". ";
}

What do we write inside @counter-style? Descriptors! How many descriptors? Honestly, a lot. Just look at this quick review of all of them:

  • system: specifies which algorithm will be used to construct the counter’s string representation. (Obligatory)
  • negative: specifies the counter representation if the counter value is negative. (Optional)
  • prefix: specifies a character that will be attached before the marker representation and any negative sign. (Optional)
  • suffix: specifies a character that will be attached after the marker representation and any negative sign. (Optional)
  • range: specifies the range in which the custom counter is used. Counter values outside the range will drop to their fallback counter style. (Optional)
  • pad: specifies a minimum width all representations have to reach. Representations shorter than the minimum are padded with a character. (Optional)
  • fallback: specifies a fallback counter used whenever a counter style can’t represent a counter value. (Optional)
  • symbols: specifies the symbols used by the construction system algorithm. It’s obligatory unless the system is set to additive or extends.
  • additive-symbols: specifies the symbols used by the construction algorithm when the system descriptor is set to additive.
  • speak-as: specifies how screen readers should read the counter style. (Optional)

However, I’ll focus on the required descriptors first: systemsymbols and additive-symbols.

The system descriptor

The symbols or additive-symbols descriptors define the characters used for the counter style, while system says how to use them.

The valid system values are:

  • cyclic
  • alphabetic
  • symbolic
  • additive
  • fixed
  • extends

cyclic will go through the characters set on symbols and repeat them. We can use just one character in the symbols to mimic a bullet list:

@counter-style cyclic-example {
  system: cyclic;
  symbols: "⏵";
  suffix: " ";
}
  • bread
  • butter
  • milk
  • apples

Or alternate between two or more characters:

@counter-style cyclic-example {
  system: cyclic;
  symbols: "🔸" "🔹";
  suffix: " ";
}
List of four items, the first two items are prefixed with an orange diamond marker and a blue diamond marker, respectively.

fixed will write the characters in symbols descriptor just one time. In the last example, only the first two items will have a custom counter if set to fixed, while the others will drop to their fallback, which is decimal by default.

@counter-style multiple-example {
  system: fixed;
  symbols: "🔸" "🔹";
  suffix: " ";
}
List of four items, the first two items are prefixed with an orange diamond marker and a blue diamond marker, respectively.

We can set when the custom counters start by appending an <integer> to the fixed value. For example, the following custom counter will start at the fourth item:

@counter-style fixed-example {
  system: fixed 4;
  symbols: "💠";
  suffix: " ";
}
List of four items, the last item has a marker formatted as a snowflake emoji.

numeric will numerate list items using a custom positional system (base-2, base-8, base-16, etc.). Positional systems start at 0, so the first character at symbols will be used as 0, the next as 1, and so on. Knowing this, we can make an ordered list using non-decimal numerical systems like hexadecimal:

@counter-style numeric-example {
  system: numeric;
  symbols: "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F";
  suffix: ". ";
}
  1. bread
  2. butter
  3. milk
  4. apples

alphabetic will enumerate the list items using a custom alphabetical system. It’s similar to the numeric system but with the key difference that it doesn’t have a character for 0, so the next digits are just repeated. For example, if our symbols are "A" "B" "C" they will wrap to "AA", "AB", "AC", then BA, BB, BC and so on.

Since there is no equivalent for 0 and negative values, they will drop down to their fallback.

@counter-style alphabetic-example {
  system: alphabetic;
  symbols: "A" "B" "C";
  suffix: ". ";
}
  1. bread
  2. butter
  3. milk
  4. apples
  5. cinnamon

symbolic will go through the characters in symbols repeating them one more time each iteration. So for example, if our symbols are "A", "B", "C", it will go “A”, “B”, and “C”, double them in the next iteration as “AA”, “BB”, and “CC”, then triple them as “AAA”, “BBB”, “CCC” and so on.

Since there is no equivalent for 0 and negative values, they will drop down to their fallback.

@counter-style symbolic-example {
  system: symbolic;
  symbols: "A" "B" "C";
  suffix: ". ";
}
  1. bread
  2. butter
  3. milk
  4. apples
  5. cinnamon

additive will give characters a numerical value and add them together to get the counter representation. You can think of it as the way we usually count bills: if we have only $5, $2, and $1 bills, we will add them together to get the desired quantity, trying to keep the number of bills used at a minimum. So to represent 10, we will use two $5 bills instead of ten $1 bills.

Since there is no equivalent for negative values, they will drop down to their fallback.

@counter-style additive -example {
  system: additive;
  additive-symbols: 5 "5️⃣", 2 "2️⃣", 1 "1️⃣";
  suffix: " ";
}
List of five grocery items with the markers formatted as emoji numbers.

Notice how we use additive-symbols when the system is additive, while we use just symbols for the previous systems.

extends will create a custom style from another one but with modifications. To do so, it takes a <counter-style-name> after the extends value. For example, we could change the decimal counter style default’s suffix to a closing parenthesis (")")`:

@counter-style extends-example {
  system: extends decimal;
  suffix: ") ";
}
  1. bread
  2. butter
  3. milk
  4. cinnamon

Per spec, “If a @counter-style uses the extends system, it must not contain a symbols or additive-symbols descriptor, or else the @counter-style rule is invalid.”

The other descriptors

The negative descriptor allows us to create a custom representation for a list’s negative values. It can take one or two characters: The first one is prepended to the counter, and by default it’s the hyphen-minus ("-"). The second one is appended to the symbol. For example, we could enclose negative representations into parenthesis (2), (1), 0, 1, 2:

@counter-style negative-example {
  system: extends decimal;
  negative: "(" ")";
}
  1. bread
  2. butter
  3. milk
  4. apples

The prefix and suffix descriptors allow us to prepend and append, respectively, a character to the counter representation. We can use it to add a character at the beginning of each counter using prefix:

@counter-style prefix-suffix-example {
  system: extends decimal;
  prefix: "(";
  suffix: ") ";
}
  • bread
  • butter
  • milk
  • apples

The range descriptor defines an inclusive range in which the counter style is used. We can define a bounded range by writing one <integer> next to another. For example, a range of 2 4 will affect elements 2, 3, and 4:

@counter-style range-example {
  system: cyclic;
  symbols: "‣";
  suffix: " ";
  range: 2 4;
}
  • bread
  • butter
  • milk
  • apples
  • cinnamon

On the other hand, using the infinite value we can unbound the range to one side. For example, we could write infinite 3 so all items up to 3 have a counter style:

@counter-style range-example {
  system: alphabetic;
  symbols: "A" "B" "C";
  suffix: ". ";
  range: infinite 3;
}
  • bread
  • butter
  • milk
  • apples
  • cinnamon

The pad descriptor takes an <integer> that represents the minimum width for the counter and a character to pad it. For example, a zero-padded counter style would look like the following:

@counter-style pad-example {
  system: extends decimal;
  pad: 3 "0";
}
  • bread
  • butter
  • milk
  • apples

The fallback descriptor allows you to define which counter style should be used as a fallback whenever we can’t represent a specific count. For example, the following counter style is fixed and will fallback to lower-roman after the sixth item:

@counter-style fallback-example {
  system: fixed;
  symbols: "⚀" "⚁" "⚂" "⚃";
  fallback: lower-roman;
}
  • bread
  • butter
  • milk
  • apples
  • cinnamon

Lastly, the speak-as descriptor hints to speech readers on how the counter style should be read. It can be:

  • auto Uses the system default.
  • bullets reads an unordered list. By default, cyclic systems are read as bullets
  • numbers reads the counter’s numeric value in the content language. By default, additivefixednumeric, and, symbolic are read as numbers.
  • words reads the counter representation as words.
  • spell-out reads the counter representation letter by letter. By default, alphabetic is read as spell-out.
  • <counter-style-name> It will use that counter’s speak-as value.
@counter-style speak-as-example {
  system: extends decimal;
  prefix: "Item ";
  suffix: " is ";
  speak-as: words;
}

symbols()

The symbols() function defines an only-use counter style without the need to do a whole @counter-style, but at the cost of missing some features. It can be used inside the list-style-type property and the counter() and counters() functions.

ol {
  list-style-type: symbols(cyclic "🥬");
}

However, its browser support is appalling since it’s only supported in Firefox.

Images in Counters

In theory, there are four ways to add images to lists:

  1. list-style-image property
  2. content property
  3. symbols descriptor in @counter-style
  4. symbols() function.

In practice, the only supported ways are using list-style-image and content, since support for images in @counter-style and support in general for symbols() isn’t the best (it’s pretty bad).

list-style-image

The list-style-image can take an image or a gradient. In this case, we want to focus on images but gradients can also be used to create custom square bullets:

li {
  list-style-image: conic-gradient(red, yellow, lime, aqua, blue, magenta, red);
}
  • bread
  • butter
  • milk
  • apples

Sadly, changing the shape would require styling more the ::marker and this isn’t currently possible.

To use an image, we pass its url(), make sure is small enough to work as a counter:

li {
  list-style-image: url("./logo.svg");
}
  • bread
  • milk
  • butter
  • apples

content

The content property works similar to list-style-image: we pass the image’s url() and provide a little padding on the left as an empty string:

li::marker {
  content: url("./logo.svg") " ";
}
A list with four grocery items. The marker is the CSS-Tricks logo.

Spacing Things Out

You may notice in the last part how the image — depending on its size — isn’t completely centered on the text, and also that we provide an empty string on content properties for spacing instead of giving things either a padding or margin. Well, there’s an explanation for all of this, as since spacing is one of the biggest pain points when it comes to styling lists.

Margins and paddings are wacky

Spacing the ::marker from the list item should be as easy as increasing the marker’s or list margin, but in reality, it takes a lot more work.

First, the padding and margin properties aren’t allowed in ::marker. While lists have two types of elements: the list wrapper (usually ol or ul) and the list item (li), each with a default padding and margin. Which should we use?

You can test each property in this demo by Šime Vidas in his article dedicated to the gap after the list marker:

You’ll notice how the only property that affects the spacing within ::marker and the text is the li item’s padding property, while the rest of the spacing properties will move the entire list item. Another thing to note is even when the padding is set to 0px, there is a space after the ::marker. This is set by browsers and will vary depending on which browser you’re using.

list-style-position

One last thing you may notice in the demo is a checkbox for the list-style-position property, and how once you set it to inside, the ::marker will move to the inside of the box, at the cost of removing any spacing given by the list item’s padding.

By default, markers are rendered outside the ul element’s box. A lot of times, this isn’t the best behavior: markers sneak out of elements, text-align won’t align the marker, and paradoxically, centered lists with flex or grid won’t look completely centered since the markers are outside the box.

To change this we can use the list-style-position property, it can be either outside (default) and inside to define where to position the list marker: either outside or the outside of the ul box.

ul {
  border: solid 2px red;
}

.inside {
  list-style-position: inside;
}

.outside {
  list-style-position: outside;
}
  • bread
  • butter
  • milk
  • apple

content with empty strings

In the same article, Šime says:

Appending a space to content feels more like a workaround than the optimal solution.

And I completely agree that’s true, but just using ::marker there isn’t a correct way to add spacing between the ::marker and the list text, especially since most people prefer to set list-style-position to inside. So, as much as it pains me to say it, the simplest way to increase the gap after the marker is to suffix the content property with an empty string:

li::marker {
    content: "•   ";
}
  • bread
  • milk
  • butter
  • apples

BUT! This is only if we want to be purists and stick with the ::marker pseudo-element because, in reality, there is a much better way to position that marker: not using it at all.

Just use ::before

There is a reason people love using the ::before more than ::marker. First, we can’t use something like CSS Grid or Flexbox since changing the display of li to something other than list-item will remove the ::marker, and we can set the ::marker‘s height or width properties to better align it.

Let’s be real, ::marker works fine when we just want simple styling. But we are not here for simple styling! Once we want something more involved, ::marker will fall short and we’ll have to use the ::before pseudo-element.

Using ::before means we can use Flexbox, which allows for two things we couldn’t do before:

  • Vertically center the marker with the text
  • Easily increase the gap after the marker

Both can be achieved with Flexbox:

li {
  display: flex;
  align-items: center; /* Vertically center the marker */
  gap: 20px; /* Increases the gap */

  list-style-type: none;
}

The original ::marker is removed by changing the display.

Accesibility

In a previous section we turned things that weren’t lists into seemingly looking lists, so the question arises: should we actually do that? Doesn’t it hurt accessibility to make something look like a list when it isn’t one? As always, it depends. For a visual user, all the examples in this entry look all right, but for assistive technology users, some examples lack the necessary markup for accessible navigation.

Take for example our initial demo. Here, listing titles serves as decoration since the markup structure is given by the titles themselves. It’s the same deal for the counting siblings demo from earlier, as assistive technology users can read the document through the title structure.

However, this is the exception rather than the norm. That means a couple of the examples we looked at would fail if we need the list to be announced as a list in assistive technology, like screen readers. For example this list we looked at earlier:

<div class="index">
  <h2>The Old Buccaneer</h2>
  <h2>The Sea Cook</h2>
  <h2>My Shore Adventure</h2>
  <h2>The Log Cabin</h2>
  <h2>My Sea Adventure</h2>
  <h2>Captain Silver</h2>
</div>

…should be written as a list instead:

<ul class="index">
  <li>The Old Buccaneer</li>
  <li>The Sea Cook</li>
  <li>My Shore Adventure</li>
  <li>The Log Cabin</li>
  <li>My Sea Adventure</li>
  <li>Captain Silver</li>
</ul>

Listing elements is rarely used just as decoration, so as a rule of thumb, use lists in the markup even if you are planning to change them with CSS.

Almanac References

List Properties

Counters

Custom Counter Styles

Pseudo-Elements

More Tutorials & Tricks!


Styling Counters in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.