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>
- Bread
- Milk
- Butter
- 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>
- Bread
- Milk
- Butter
- Apples
- Bread
- Milk
- Butter
- 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 alphabeticA
for uppercase alphabetici
for lowercase Roman numbersI
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>
- Bread
- Milk
- Butter
- Apples
- Bread
- Milk
- Butter
- 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>
- Bread
- Milk
- Butter
- 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>
- Bread
- Milk
- Butter
- 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>
- Bread
- Milk
- Butter
- 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 reversed
, start
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 h
4 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 ::marker
, list-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;
}
- bread
- 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: "➡️ ";
}
Creating Custom Counters
For a long time, there wasn’t a CSS-equivalent to the HTML reverse
, start
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:
- Initiate the counter with the
counter-reset
property. - Increment the counter with the
counter-increment
property. - Individually set the counters with the
counter-set
property. - Output the counters with either the
counter()
andcounters()
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) ": ";
}
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, ".") ". ";
}
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) ": ";
}
li::marker {
content: counters(item, ".", lower-alpha) ". ";
}
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) ". ";
}
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 theirfallback
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 constructionsystem
algorithm. It’s obligatory unless thesystem
is set toadditive
orextends
.additive-symbols
: specifies the symbols used by the construction algorithm when thesystem
descriptor is set toadditive
.speak-as
: specifies how screen readers should read the counter style. (Optional)
However, I’ll focus on the required descriptors first: system
, symbols
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: " ";
}
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: " ";
}
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: " ";
}
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: ". ";
}
- bread
- butter
- milk
- 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: ". ";
}
- bread
- butter
- milk
- apples
- 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: ". ";
}
- bread
- butter
- milk
- apples
- 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: " ";
}
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: ") ";
}
- bread
- butter
- milk
- 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: "(" ")";
}
- bread
- butter
- milk
- 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 thesystem
default.bullets
reads an unordered list. By default,cyclic
systems are read asbullets
numbers
reads the counter’s numeric value in the content language. By default,additive
,fixed
,numeric
, and,symbolic
are read asnumbers
.words
reads the counter representation as words.spell-out
reads the counter representation letter by letter. By default,alphabetic
is read asspell-out
.<counter-style-name>
It will use that counter’sspeak-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:
list-style-image
propertycontent
propertysymbols
descriptor in@counter-style
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") " ";
}
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
Almanac
on
Apr 23, 2021
list-style
Counters
Almanac
on
Feb 4, 2025
counter-reset
Almanac
on
Jan 14, 2025
counter-increment
Almanac
on
Apr 23, 2021
counter-set
Almanac
on
Feb 4, 2025
counter()
h2::before { content: counter(my-counter, upper-roman) ". "; }
Almanac
on
Feb 4, 2025
counters()
Custom Counter Styles
Almanac
on
Jan 28, 2025
@counter-style
Almanac
on
Jan 30, 2025
symbols()
Pseudo-Elements
Almanac
on
Jan 19, 2025
::marker
Almanac
on
Sep 13, 2024
::before / ::after
More Tutorials & Tricks!
Article
on
May 5, 2020
List Style Recipes
Article
on
Apr 29, 2021
List Markers and String Styles
Article
on
May 19, 2018
Style List Markers in CSS
Article
on
Jun 11, 2020
How to Reverse CSS Custom Counters
Article
on
Jan 23, 2025
Some Things You Might Not Know About Custom Counter Styles
Article
on
Jan 26, 2022
Using CSS Counters for Custom List Number Styling
Article
on
May 17, 2024
Everything You Need to Know About the Gap After the List Marker
Styling Counters in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.