The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Insert content before.note::before { content: "Note: "; }View examples
Read an attribute.tag::after { content: " [" attr(data-state) "]"; }View examples
Suppress generated content.note:empty::before { content: none; }View examples
Generate a line break.label::after { content: "\A"; white-space: pre; }View examples
Create a counter.procedure { counter-reset: step; }View examples
Increment a counter.procedure > li { counter-increment: step; }View examples
Render a counter.procedure > li::marker { content: "Step " counter(step) ": "; }View examples
Choose a counter style.appendix::before { content: counter(appendix, upper-alpha) ". "; }View examples
Scope nested counters.outline, .outline ol { counter-reset: item; }View examples
Render the counter chain.outline li::marker { content: counters(item, ".") " "; }View examples
Define quote pairs:lang(fr) { quotes: "« " " »" "‹ " " ›"; }View examples
Generate quotation marks.quotation::before { content: open-quote; } .quotation::after { content: close-quote;; }View examples
Style list markers.tasks > li::marker { color: #0f766e; font-weight: 700; }View examples
Replace marker content.tasks > li::marker { content: "✓ "; }View examples
Use native list numberingol > li::marker { content: counter(list-item) ". "; }View examples
Keep essential text in HTML<button><span aria-hidden="true">✓</span> Save changes</button>View examples

Generated content is best used to decorate real HTML or derive presentational labels that are safe to lose. CSS counters can number headings and nested outlines, quote keywords can select language-appropriate quotation marks, and ::marker can style genuine list items. Keep names, instructions, status, and other essential meaning in the document because generated text is not a dependable substitute for semantic content or accessible names.

Step by step

Detailed examples

01

Generate short decorative strings and attribute values

The content property supplies the contents of ::before and ::after. Adjacent strings and attr() results are concatenated, while content: none suppresses the pseudo-element entirely. A \A escape becomes a line feed, but it only produces a visible break when white-space handling preserves it. Attribute-derived text is captured from the originating element and should remain presentational: changing a data attribute does not make CSS a reliable announcement channel.

Decorative note labels and state annotations
<p class="note">Back up the database before migrating.</p>
<span class="tag" data-state="stable">API</span>
Generate the decorations
.note::before {
  content: "Note: ";
  font-weight: 700;
}

.tag::after {
  content: " [" attr(data-state) "]";
  color: #475569;
}

.note:empty::before { content: none; }
.label::after { content: "\A"; white-space: pre; }

Note: Do not put a required warning, field label, or control name only in generated content.

Back to quick reference ↑
02

Reset, increment, and render automatic numbers

counter-reset creates a named counter instance, normally starting at zero, and counter-increment changes the innermost instance by one unless another amount is supplied. counter() renders the current value and accepts an optional counter style. Attach increments to the repeated source element rather than its decorative pseudo-element so the numbering model remains easy to inspect.

Number a procedure with descriptive markers
.procedure { counter-reset: step; }

.procedure > li {
  counter-increment: step;
  padding-block: 0.5rem;
}

.procedure > li::marker {
  content: "Step " counter(step) ": ";
  font-weight: 700;
}

.appendices { counter-reset: appendix; }
.appendix { counter-increment: appendix; }
.appendix::before { content: counter(appendix, upper-alpha) ". "; }
Back to quick reference ↑
03

Build hierarchical outline numbers with counter scope

Resetting the same name on each nested list creates a new counter instance in that scope. Each list item increments its nearest item counter, and counters() serializes the chain of visible instances from outermost to innermost. Scope resets on the list containers, not every list item, or sibling numbering will repeatedly restart.

Render 1, 1.1, and 1.2-style outline labels
.outline,
.outline ol {
  counter-reset: item;
}

.outline li { counter-increment: item; }
.outline li::marker {
  content: counters(item, ".") " ";
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}
Back to quick reference ↑
04

Choose quotation marks by language and nesting depth

The inherited quotes property defines pairs in outer-to-inner order. open-quote and close-quote insert the pair for the current quote depth while updating that depth; the depth follows generated quote occurrences in the flattened element tree rather than simple DOM nesting. Native q elements already have browser quotation behavior, so explicit pseudo-elements are most useful for a controlled decorative treatment.

Language-aware generated quotation marks
:root { quotes: "“" "”" "‘" "’"; }
:lang(fr) { quotes: "« " " »" "‹ " " ›"; }

.quotation::before { content: open-quote; }
.quotation::after { content: close-quote; }

Note: Set a correct lang attribute in HTML so language selectors and assistive technologies have the same source of truth.

Back to quick reference ↑
05

Customize markers on real list items

Only elements whose display computes to list-item generate ::marker. Marker boxes support content plus a deliberately limited property set, although inheritable text properties such as color and font characteristics can affect their text. Ordered list items maintain an implicit list-item counter, so custom numbering can reuse it without creating a parallel counter. Preserve ol, ul, and li markup when the content is structurally a list.

Style task and ordered-list markers
.tasks > li::marker {
  content: "✓  ";
  color: #0f766e;
  font-weight: 700;
}

ol > li::marker {
  content: counter(list-item) ". ";
  color: #1d4ed8;
  font-variant-numeric: tabular-nums;
}
Back to quick reference ↑
06

Keep meaning, names, and state out of presentation-only content

Generated content is not a DOM text node, and its exposure to accessibility APIs has varied across browsers and assistive technologies. Essential labels, instructions, error messages, and status changes belong in HTML, where they can participate predictably in accessible-name and live-region computation. Use CSS for redundant decoration; when a visible symbol is in markup, hide only that decorative symbol from assistive technology and retain a real text label.

A named control with a decorative symbol
<button class="save-button" type="button">
  <span aria-hidden="true">✓</span>
  Save changes
</button>
Decorate without replacing the control name
.save-button {
  display: inline-flex;
  gap: 0.45rem;
  align-items: center;
}

.save-button > [aria-hidden="true"] { color: #15803d; }

Note: A pseudo-element icon is also suitable when it is purely decorative; the important point is that Save changes remains real text.

Back to quick reference ↑

Local code tester

Try counters, quotes, and markers

Edit a semantic procedure, quotation, and task list while keeping their essential text in the HTML.

Runs in your browser
Preview

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. World Wide Web ConsortiumCSS Generated Content Module Level 3w3.org
  2. World Wide Web ConsortiumCSS Lists and Counters Module Level 3w3.org
  3. World Wide Web ConsortiumCSS Counter Styles Level 3w3.org
  4. MDN Web DocsCSS lists and countersdeveloper.mozilla.org
  5. MDN Web DocsUsing CSS countersdeveloper.mozilla.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback