Complete CSS Course Module 11: Intermediate: Floats and Visual Effects

CSS Counters and Generated Content

  • Beginner
  • Lesson 11.9
  • 5 min read
  • 50 of 77 in the course

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::marker is 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-increment and counter() inside content.
  • Restarting a sub-counter on each heading gives numbering like 1.1, 1.2, 2.1.
  • Counters plus ::before make custom step indicators while keeping the <ol>.
  • The quotes property controls the marks for <q>, open-quote and close-quote.
  • Use generated content for decoration only, and give icons empty alt text.