SymbolFYI

CSS Content Property: Using Unicode Symbols in Stylesheets

CSS gives you two primary ways to inject Unicode characters into a page without touching HTML: the content property on pseudo-elements and the @counter-style / list-style properties for list markers. Done correctly, these techniques keep your markup clean and semantic. Done carelessly, they create invisible characters, accessibility gaps, and encoding bugs that are maddeningly hard to debug.

The content Property and Pseudo-Elements

The content property is valid only on ::before and ::after pseudo-elements (and a few others like ::marker in modern browsers). It injects generated content into the render tree — content that is visually present but does not exist in the DOM source.

/* Basic usage */
.external-link::after {
  content: " ↗";
}

.required::after {
  content: " *";
  color: red;
}

The generated content appears in the accessibility tree in most modern browsers. Screen readers will announce it, which is sometimes what you want and sometimes not.

CSS Unicode Escape Sequences

CSS uses a different escape syntax from JavaScript and HTML. The format is a backslash followed by 1–6 hexadecimal digits:

\hex-digits

No U+ prefix, no #x, no &. Just a backslash and the code point:

/* U+2192 RIGHTWARDS ARROW */
.next::after { content: "\2192"; }

/* U+1F4C4 PAGE FACING UP (emoji, needs 5 hex digits) */
.doc-link::before { content: "\1F4C4"; }

/* U+00A9 COPYRIGHT SIGN */
.copyright::before { content: "\A9"; }  /* leading zeros optional */

The escape sequence ends at the first character that is not a valid hex digit. If the character immediately after the escape sequence is a valid hex digit or a space, you need a trailing space to terminate the sequence (the space is consumed and not rendered):

/* These are different: */
content: "\41 B";   /* "A" + "B" — trailing space terminates \41 */
content: "\41B";    /* U+041B CYRILLIC CAPITAL LETTER EL (Л) */

/* Safe pattern: always pad to 6 digits to avoid ambiguity */
content: "\000041 B";  /* "A" + "B", unambiguous */

The safest practice for short code points is to pad to exactly 4 or 6 digits:

/* Preferred: 4-digit padding for BMP characters */
.checkmark::before { content: "\2713"; }  /* ✓ */
.cross::before     { content: "\2717"; }  /* ✗ */
.bullet::before    { content: "\2022"; }  /* • */

/* 5-digit for supplementary plane (> U+FFFF) */
.star::before { content: "\1F31F"; }  /* 🌟 */

Direct Unicode Characters vs. Escape Sequences

Like HTML, you can also write the character directly in a CSS string if your stylesheet is UTF-8:

/* Equivalent forms — both work in UTF-8 CSS */
.arrow::after { content: "→"; }
.arrow::after { content: "\2192"; }

The direct form is more readable but depends on:

  1. The file being saved as UTF-8
  2. The server sending the correct Content-Type: text/css; charset=UTF-8 header (or a @charset "UTF-8"; declaration at the top of the file)
  3. Your build tools not mangling the encoding

The escape form is safer for characters outside ASCII, especially in projects where multiple developers use different editors or when CSS is generated by tools that may not be Unicode-aware.

/* Add this as the very first line of your CSS file */
@charset "UTF-8";

/* Now direct Unicode is unambiguous */
.em-dash::after { content: "—"; }

Custom List Markers

Before ::marker and list-style-type: symbols(), the standard approach was to remove default list styling and use ::before on <li> elements:

/* Classic approach */
ul.custom {
  list-style: none;
  padding-left: 0;
}

ul.custom li::before {
  content: "\2023 "; /* ‣ TRIANGULAR BULLET + space */
  color: #0066cc;
  font-weight: bold;
}

Modern CSS gives you ::marker which is cleaner because it does not affect list item indentation the way ::before does:

/* Modern approach — browser support: all major browsers */
ul.custom li::marker {
  content: "\27A4  "; /* ➤ + two spaces for gap */
  color: #0066cc;
}

/* Ordered list with custom counter */
ol.steps {
  counter-reset: step-counter;
  list-style: none;
}

ol.steps li {
  counter-increment: step-counter;
}

ol.steps li::before {
  content: counter(step-counter, decimal) ". ";
  font-weight: bold;
  color: #333;
}

For fully custom counter glyphs, @counter-style is the most powerful option:

@counter-style circled-digits {
  system: cyclic;
  symbols: "\2460" "\2461" "\2462" "\2463" "\2464";
  /* ① ② ③ ④ ⑤ */
  suffix: " ";
}

ol.circled {
  list-style-type: circled-digits;
}

Decorative Separators and Dividers

A common pattern is using ::before or ::after to inject separator characters between inline elements:

/* Pipe separator between nav items */
nav a + a::before {
  content: " \7C "; /* | */
  color: #ccc;
  margin: 0 0.25em;
}

/* Bullet separator */
.breadcrumb li + li::before {
  content: " \00BB "; /* » */
  padding: 0 0.5em;
}

/* Em dash separator for definition lists */
dt::after {
  content: " \2014 "; /* — */
}

Quotation Marks with quotes and content

CSS handles language-appropriate quotation marks through the quotes property combined with content: open-quote and content: close-quote:

/* English */
:lang(en) { quotes: "\201C" "\201D" "\2018" "\2019"; }
/* " " ' ' */

/* French */
:lang(fr) { quotes: "\AB\A0" "\A0\BB" "\2039\A0" "\A0\203A"; }
/* « [space] [space] » ‹ [space] [space] › */

/* German */
:lang(de) { quotes: "\201E" "\201C" "\201A" "\2018"; }
/* „ " ‚ ' */

q::before { content: open-quote; }
q::after  { content: close-quote; }

This is far better than hardcoding quotation marks in CSS or HTML, because it adapts to the document's language automatically when you use the lang attribute correctly.

Accessibility: The Critical Consideration

Generated content via CSS content is read by screen readers in most modern browsers. This is sometimes desirable (an arrow after a link indicating it opens externally) and sometimes not (decorative bullet points that clutter the reading experience).

Hiding decorative content from assistive technology

CSS content with speak: none was the old approach but speak is no longer in the CSS spec. The correct modern technique uses aria-hidden on a separate element, or the alt parameter of content:

/* CSS Content Level 3 — the alt text form */
/* content: "visual" / "alt text for AT" */

/* Empty alt text = hidden from AT */
.decorative-bullet::before {
  content: "\2726" / ""; /* ✦ visually, empty string to AT */
}

/* Meaningful alt text */
.external-link::after {
  content: "\2197" / "(opens in new tab)";
  /* ↗ visually, spoken text for screen readers */
}

The content: "visual" / "alt" syntax has good support in modern browsers (Chrome 86+, Firefox 117+, Safari 17+). For older browser support, the workaround is to use aria-hidden="true" on a <span> containing the decorative character in HTML, rather than relying on CSS:

<!-- Accessible external link with visual indicator -->
<a href="https://example.com">
  Visit Example
  <span aria-hidden="true"></span>
  <span class="sr-only">(opens in new tab)</span>
</a>
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

Do not put essential information in CSS content

CSS is presentation. A user who disables stylesheets, or a search engine bot, will not see content generated by CSS:

/* Wrong — essential content in CSS */
.price::before { content: "$"; }
.error::before { content: "Error: "; }

/* Correct — semantic content in HTML, decorative styling in CSS */
/* <span class="currency">$</span>49.99 */
/* <span class="error-label">Error:</span> File not found */

Font Coverage Considerations

CSS-generated Unicode characters are subject to the same font fallback rules as regular text. A character that displays correctly in body text may render as a tofu box (□) when injected via content if your pseudo-element inherits a font that lacks that glyph.

/* Specify a fallback stack that covers your symbols */
.status-icon::before {
  font-family: "Segoe UI Emoji", "Apple Color Emoji", "Noto Emoji",
               "Segoe UI Symbol", "Symbola", sans-serif;
  content: "\2714"; /* ✔ */
}

For emoji specifically, the color emoji fonts need to be listed before symbol fonts to get colored rendering rather than monochrome:

/* Colored emoji in pseudo-elements */
.success::before {
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
               "Twemoji Mozilla", sans-serif;
  content: "\2705"; /* ✅ */
}

Practical Patterns

Progress and status indicators

[data-status="complete"]::before {
  content: "\2714\FE0F "; /* ✔️ */
  font-family: "Apple Color Emoji", "Segoe UI Emoji", sans-serif;
}

[data-status="pending"]::before {
  content: "\23F3 "; /* ⏳ */
  font-family: "Apple Color Emoji", "Segoe UI Emoji", sans-serif;
}

[data-status="error"]::before {
  content: "\274C "; /* ❌ */
  font-family: "Apple Color Emoji", "Segoe UI Emoji", sans-serif;
}

Code block copy button label

.code-block .copy-button::before {
  content: "\29C9 "; /* ⧉ DUPLICATE, commonly used for "copy" */
}

.code-block .copy-button.copied::before {
  content: "\2713 "; /* ✓ */
}
@media print {
  a[href]::after {
    content: " (" attr(href) ")";
    font-size: 0.85em;
    color: #555;
  }
}

Use the SymbolFYI Encoding Converter to find the CSS escape sequence for any Unicode character — look for the "CSS Escape" column in the output.

Debugging CSS Unicode Issues

When a symbol appears as a box or question mark:

  1. Check the font does not have the glyph — add a font family with better symbol coverage
  2. Check the escape sequence is correct — an extra or missing hex digit produces the wrong character entirely
  3. Check the trailing space ambiguity — \41B is not \41 + B
  4. Verify the CSS file charset — missing @charset "UTF-8" can cause mojibake on direct characters
  5. Open browser DevTools, inspect the pseudo-element, and copy the rendered character to confirm what was actually generated

Next in Series: Unicode-Aware Regex: Property Escapes and Multilingual Patterns — writing regex that correctly handles letters from any script, emoji, and Unicode categories across JavaScript and Python.

İlgili Semboller

İlgili Sözlük

İlgili Araçlar

Daha Fazla Kılavuz