skip to content

When is CSS generated content (a content value on ::before or ::after) the wrong place to put text, and what breaks if meaningful copy lives there?

level: seniorimportance: should knowfreq 38%

answer

  1. it lives in the stylesheet, not the document
  2. try selecting it with the mouse
  3. find-in-page and translation both walk the DOM
  4. icon glyphs can be announced as nonsense
  5. would deleting it lose meaning or polish

basics

~20 s

Generated content is a rendering instruction, not document text: it cannot be selected, copied or found with in-page search, translation tools skip it, and how assistive technology announces it varies by engine. Keep it decorative and put meaningful copy in the document.

solid answer

~50 s

`content` on `::before`/`::after` produces glyphs that exist only in the rendering. They are not part of the document, so a user cannot select or copy them, the browser's find-in-page will not match them, and machine translation generally leaves them alone — which quietly breaks localisation because the string is trapped in a stylesheet nobody's translation pipeline reads. Announcement by assistive technology is the murky part: modern engines usually do expose generated text, which is worse than not exposing it when the string is a private-use icon-font code point that reads as a nonsense character. So the rule is: use `content` for decoration, counters and quotation marks, never for a label, a price, an error message or anything a user must be able to read, search or copy. Where a decorative glyph might be announced, the alternative-text syntax `content: "★" / ""` lets you supply an empty alternative, though engine support for it is uneven.

code

css · 13 lines
css
/* fine: decorative, with an empty alternative */
.breadcrumb li + li::before {
  content: "/" / "";
  padding-inline: 6px;
  color: #888;
}

/* fine: presentational numbering */
.steps { counter-reset: step; }
.steps li::before {
  counter-increment: step;
  content: counter(step) ". ";
}

go deeper

for a junior

Keep generated content decorative — arrows, separators, bars. If the user needs to read, search or copy the words, they belong in the page itself and not in a content value.

for a middle

Be able to list the concrete failures: no selection, no copy, no find-in-page match, no translation, and no visibility to tests that read rendered text. Explain that content is a rendering instruction.

for a senior

Correct the outdated claim that assistive tech ignores generated content, and explain why exposure makes icon-font code points worse rather than safer. Know the alternative-text syntax and that support for it is uneven.

for a principal

Own the boundary as a reviewable rule across the codebase: would deleting this string cost information or only polish? Decide where decorative content may live so localisation and support tooling never has to chase text trapped in stylesheets.

## Generated content is not document content The distinction that drives every consequence here is simple: `::before` and `::after` produce boxes described by a stylesheet, not nodes that exist in the document. CSS is a presentation layer, and text living in the presentation layer is invisible to everything that operates on document text. ```css /* the word "Sale" exists only in this file */ .badge::before { content: "Sale"; } ``` ## What concretely breaks **Selection and copy.** A user cannot drag-select generated text, and copying the surrounding region will not include it. If a price, an order number or an error code lives in `content`, the user cannot copy it — a real support burden. **Find-in-page.** The browser's own search matches document text. Generated glyphs are not in that index, so a user searching for a label they can plainly see on screen gets no result. **Translation.** Both browser translation and localisation pipelines walk the document. A string in a stylesheet is not in scope for either, so a page that otherwise translates cleanly keeps a handful of untranslated words. Worse, it fails silently: nobody notices until a user in another locale reports it. **Assistive technology.** This one is more nuanced than the folklore suggests. It used to be repeated that screen readers ignore generated content; in practice, current engines usually *do* expose it, because CSS-generated text participates in the accessible name computation. That makes an icon-font glyph the dangerous case: a private-use-area code point has no meaningful pronunciation, so what gets announced is a garbled character name or nothing coherent. **Testability.** Automated tests that query rendered text do not see generated content either, so a regression in a `content`-based label is invisible to the test suite. ## The alternative-text syntax CSS Generated Content defines a way to supply an alternative for generated content, separated by a slash: ```css .icon-warning::before { content: "\26A0" / ""; /* decorative: no alternative announced */ } .status::before { content: url(check.svg) / "complete"; } ``` An empty alternative marks the glyph as purely decorative; a string supplies text to use in its place. Engine support has been landing across browsers but is not uniform, so treat it as an improvement rather than a guarantee — verify in the browsers you support before you rely on it, and do not let a required meaning depend on it. ## Where generated content is exactly right The feature is not a hazard; it is a presentation tool being asked to do a document job. Legitimate uses: - **Pure decoration**: separator glyphs, chevrons, gradient bars, overlay scrims, focus rings drawn as a box. - **Counters**: `content: counter(step) ". "` produces numbering that is genuinely presentational — the numbers are a rendering of order, not information the user needs to copy. - **Quotation marks**: `open-quote` / `close-quote` render the current `quotes` value, which is locale-dependent punctuation, precisely a styling concern. - **`attr()` mirroring**: `content: attr(data-label)` is a partial exception, since the string does live in the document — but it is still not selectable, and the attribute itself may not be in a translation pipeline. ## A worked decision Suppose a design shows a required-field asterisk, an icon-only button, and a validation message. The asterisk is decoration paired with a real label, so `content: " *"` is fine — ideally with an empty alternative so it is not announced twice. The icon-only button is the trap. If the only thing conveying "delete" is a glyph in `content`, the meaning exists nowhere but the stylesheet. The fix is structural: the button carries real text in the document, and CSS decides how that text is presented visually. The validation message is unambiguous — it is content the user must read, search, copy into a support ticket, and have translated. It belongs in the document, full stop. ## The review rule A compact test that works in code review: *if the string were deleted from the stylesheet, would the page lose information or only lose polish?* Losing information means it was never presentational and should not have been in `content`. Losing polish means `content` was the right tool. Codifying that one question catches almost every instance before it ships.

  • Is it true that screen readers simply ignore ::before content?
    That was once the common advice and it is no longer accurate. Current engines generally do expose CSS-generated text, because it participates in the accessible name computation. That makes icon-font glyphs the real hazard rather than a safe one: a private-use code point has no sensible pronunciation. Use the alternative-text syntax to supply an empty alternative for decorative glyphs where it is supported.
  • Does content: attr(data-label) escape the problem, since the string is in the document?
    Only partly. The string does live in the document as an attribute, so it can be translated by pipelines that handle attributes and it is visible to tests that read attributes. But the rendered glyphs are still generated, so they remain unselectable and unmatched by find-in-page. If the user must be able to read and copy the value, render it as real text and use CSS only for its presentation.
  • How would you enforce this boundary in a large codebase?
    Make it a review question rather than a lint rule, since a linter cannot tell a chevron from a price. Ask whether deleting the string would cost information or only polish. Where a rule is wanted, ban string literals in `content` outside a designated decoration layer, allowing empty strings, counters and quote keywords, which covers the legitimate cases without false positives.

saying these in an interview costs you the question

  • Puts a button's only label in a content value
  • Assumes generated content is always ignored by screen readers
  • Forgets that find-in-page cannot match generated text
  • Ships icon-font glyphs with no textual alternative
  • Treats stylesheet strings as translatable copy

context