In short
CSS counters number things automatically: headings, custom ordered lists and step indicators. You'll also learn the quotes property and how to keep generated content accessible. What are CSS counters?
CSS counters number things automatically: headings, custom ordered lists and step indicators. You'll also learn the quotes property and how to keep generated content accessible.
What are CSS counters?
A CSS counter is a number that CSS keeps track of for you, like a person at the door with a clicker counting guests. You create the counter, tell CSS when to add one, and then show the number with ::before or ::after. You need three things:
counter-reset: name;creates (or restarts) a counter at 0, usually on a parent.counter-increment: name;adds 1 each time an element appears.content: counter(name);shows the current value inside a pseudo-element.
How to number headings automatically
Long guides and documentation often number their sections: 1, 2, 3, and sub-sections 1.1, 1.2. If you type the numbers by hand, you must change all of them when you add a new section. With counters, CSS does it.
<article class="guide">
<h2>Install the tools</h2>
<h3>Download a code editor</h3>
<h3>Install a browser</h3>
<h2>Create your first page</h2>
<h3>Make a folder</h3>
<h3>Write some HTML</h3>
</article>
.guide {
counter-reset: section;
}
.guide h2 {
counter-increment: section;
counter-reset: sub;
}
.guide h2::before {
content: counter(section) ". ";
color: #2563eb;
}
.guide h3 {
counter-increment: sub;
}
.guide h3::before {
content: counter(section) "." counter(sub) " ";
color: #6b7280;
}
The headings now read "1. Install the tools", "1.1 Download a code editor", "1.2 Install a browser", "2. Create your first page", "2.1 Make a folder" and so on. Each h2 restarts the sub counter, so sub-sections start again at 1. Add a new h2 in the middle and every number below it updates by itself.
Tip: For nested lists of any depth, use
counters(name, ".")(with an "s"). It joins the values of all levels with the separator you choose, giving numbers like 1.2.3 automatically.
Custom ordered lists and step indicators
The normal numbers of an <ol> are hard to design. With a counter you can put each number in a colored circle. This is a great way to make a "how it works" step list.
<ol class="steps">
<li>Create a free account</li>
<li>Choose your first course</li>
<li>Finish one lesson a day</li>
</ol>
.steps {
list-style: none;
padding: 0;
counter-reset: step;
}
.steps li {
counter-increment: step;
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.steps li::before {
content: counter(step);
display: grid;
place-items: center;
width: 36px;
height: 36px;
border-radius: 50%;
background: #2563eb;
color: white;
font-weight: bold;
}
Each step now has a blue circle with a white number: 1, 2, 3. We removed the normal numbers with list-style: none but kept the <ol>, so screen readers still know it is an ordered list.
You can also use a different number style, for example counter(step, upper-alpha) for A, B, C, or counter(step, decimal-leading-zero) for 01, 02, 03.
Note: If you only need simple changes, like a colored number,
li::markeris easier. Use counters when you need full control, like circles, or when you number things that are not list items.
Smart quotation marks with the quotes property
The <q> element is for short quotes inside a sentence. The browser adds quotation marks automatically using the quotes property, and you can change which marks it uses. The first pair is for outer quotes, the second for quotes inside quotes.
<p class="review">Maria said: <q>The teacher told us <q>practice every day</q>, and it worked.</q></p>
.review q {
quotes: "“" "”" "‘" "’";
}
.fancy-quote::before {
content: open-quote;
}
.fancy-quote::after {
content: close-quote;
}
The outer quote gets curly double marks and the inner quote gets single marks. With content: open-quote and close-quote, any element (like a styled <blockquote> with class fancy-quote) can use the same marks. Different languages use different marks, for example « » in French, and the browser often picks the right ones from the page's lang attribute.
Is generated content accessible?
Text added with content is shown on screen, and most screen readers read it out too. But users can't select or copy it, search engines may ignore it, and it doesn't exist if CSS fails to load. So follow this rule: use generated content for decoration and numbering, never for important information.
For decorative icons, you can give empty alternative text after a slash, so screen readers skip it:
.new-badge::before {
content: "★" / "";
}
The star is visible, but screen readers don't announce "black star" before every badge. You can also write real alt text there: content: "★" / "Featured";. Read more on MDN's CSS counters guide.
Common mistake: Putting a price, a warning or a button label in
content. If it matters to the user, write it in the HTML.
Practice exercise
Create a "Checkout steps" bar with four steps: Cart, Address, Payment, Done. Number them with a counter inside circles, and give the step with class current a different color. Then number the h2 headings of a short FAQ page automatically as "Question 1", "Question 2" and so on.
Hint: for the FAQ, use content: "Question " counter(q) ": "; on h2::before.
Key takeaways
- Counters need
counter-reset,counter-incrementandcounter()insidecontent. - Restarting a sub-counter on each heading gives numbering like 1.1, 1.2, 2.1.
- Counters plus
::beforemake custom step indicators while keeping the<ol>. - The
quotesproperty controls the marks for<q>,open-quoteandclose-quote. - Use generated content for decoration only, and give icons empty alt text.