content: "\00a0"
Somewhere in every stylesheet I’ve kept for longer than a week there’s a content: "\00a0" and I often wonder how I’d explain that to someone new. So I’m going to try to do it here.
It’s a non-breaking space (U+00A0), the thing you’d get from in HTML. That’s the short version. The longer version is why you’d write it like that instead of just hitting the space bar. That part took me a while.
One of these is not like the others
Say you’ve got a breadcrumb and you want a slash between the items. So you write the separator into a ::before with a space on either side and it looks fine on your machine at your window width. Then at some narrower width the line wraps directly in front of the slash and now there’s a lone / hanging at the start of a line like it wandered off. A normal space is a legal place to break a line, and the browser is going to take you up on that eventually.
Collapsing is the other half of it. White space in generated content goes through the same white space processing as everything else. So a plain space next to other collapsible white space collapses down, and one sitting at the edge of a line box gets dropped entirely. That’s why content: " " on an empty element doesn’t reliably hold the element open.
\00a0 is immune to both. It won’t collapse and it won’t break.
.crumb + .crumb::before {
/* the slash stays welded to what precedes it */
content: "\00a0/\00a0";
}
.tag:empty::before {
/* an empty pill that still has a height */
content: "\00a0";
}
And you could paste a real U+00A0 in there instead. Nothing’s stopping you. But then your stylesheet contains a character that’s visually indistinguishable from a regular space in every diff and every code editor. It stays that way until the day some well-meaning tool normalizes white space or the file gets served as something other than UTF-8 and the whole thing quietly turns into a  .
Hungry escape characters
A CSS escape is a \ followed by 1 to 6 hex digits. It keeps consuming digits until it either hits 6 or hits a character that isn’t one, which is the part nobody tells you. So the sequence doesn’t end where you think it ends. It ends where the parser runs out of hex.
Which means \a0 for a non-breaking space is a landmine. content: "\a0dog" doesn’t give you a space and then “dog.” It gives you U+A0D0 (some CJK ideograph) and then “g.” No error, no warning, just a mystery glyph in your breadcrumb.
The same trap applies to \a, the escape for a newline (U+000A). That one’s useful. Give the pseudo-element white-space: pre-wrap and content: "first\a second" puts generated content on two lines. That’s how a lot of CSS-only tooltips work.
Just never write \adog because \ad is a soft hyphen and you’ll spend 20 minutes on it.
I try to keep a handful of these in my head: \00d7 for a multiplication sign (the thing a close button should be instead of the letter x!), \2022 for a bullet, \2192 for a right arrow, \2019 for a curly apostrophe, \2026 for an ellipsis, and \2014 for an em dash. I think the em dash is a lazy substitute for a semicolon or some other form of punctuation but I recognize that that’s a me thing and you do you.
No strings attached
I think content gets filed away as “the thing that makes ::before and ::after exist.” Fair enough, since a pseudo-element without a content declaration doesn’t generate at all and content: "" is more important than it looks. But the value accepts a lot more than a quoted string, and some of it is really great.
attr() pulls an attribute into the content, which gives you the one print stylesheet trick everybody should steal. Paper can’t be clicked so put the URL on the page.
@media print {
a[href^="http"]::after {
content: "\00a0(" attr(href) ")";
}
}
/* stacked table rows, labeled from the markup */
td::before {
content: attr(data-label) ":\00a0";
}
Counters are the other one I reach for and then promptly forget about. Pair counter-reset with counter-increment and you get numbering that renumbers itself when you reorder the HTML. It sounds like a small thing until the first time you’ve hand-numbered nine headings and then inserted one at the top. counters(section, ".") walks the whole nesting stack so you get 2.1.4 out of it for free.
And content on a regular element (not a pseudo) replaces the element’s children outright. So content: url(logo-inverted.svg) inside a .theme-dark block swaps a logo with no second <img> and no JS. It’s still an <img> so its alt still applies, which is more than I can say for most image-swapping tricks.
Chrome and Safari do this today. Firefox and Edge ignore the declaration and show the original image, which for a logo is a perfectly good fallback.
Decoration or duplication
Generated content isn’t text even though it looks like it. It’s not in the DOM. You can’t select it, you can’t copy it, and ctrl+f doesn’t find it.
Whether a screenreader announces it at all depends on which screenreader and which browser and who knows what else. Sometimes it gets read out when you wanted silence which is how a decorative bullet ends up being narrated. Sometimes it’s skipped when it was carrying meaning.
The spec has a syntax for the first half of that. A slash and a string sets alternative text. content: "\2022" / "" marks a decoration as decoration and content: url(trash.svg) / "Delete" gives an icon a name.
No browser has it yet. Safari has an older version of the idea, a separate alt property that only works on ::before and ::after. The spec traded that property for the slash last spring. So it’s one to watch, not one to lean on in production.
But mostly I just try to obey the rule instead of patching around it. Generated content can decorate or it can duplicate. It can’t be the only place something is said.
The URL in the print stylesheet is fine because the href is right there in the markup. The data-label on the table cell is fine for the same reason. A required-field asterisk that only exists in a ::before is not fine. I’ve definitely made that mistake more than once.
So go pad your escapes to six digits. That’s the actionable part of all this. It costs nothing and it’ll save you the 20 minutes I spent on \adog.