Senior 3 min · March 29, 2026

CSS Quotes — Why Minifiers Break Unicode Escapes

Minifiers silently drop CSS quotes rules with backslash escapes, causing quotation marks to vanish by locale.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • CSS quotes property controls the quotation marks inserted by content: open-quote / close-quote
  • Use the `quotes` rule to set custom quote pairs for any language or design
  • Nested quotes automatically use the second pair, third pair, and so on
  • Performance: no runtime overhead — quotes are static content, computed at render time
  • Production insight: quote marks disappear or appear incorrectly if `quotes` not set or inheritance broken
  • Biggest mistake: assuming `quotes` is inherited — it's not; each element must explicitly define or inherit via the cascade
Plain-English First

Imagine you run a restaurant with locations in Paris, Tokyo, and New York. Each city's menu uses a different style of quotation marks — French guillemets (« »), Japanese corner brackets (「 」), and standard English curly quotes (

Quotation marks in web content are easy to get wrong. Hardcode them as HTML entities and you'll chase broken nesting, mismatched styles, and accessibility issues across every page. The CSS `quotes property solves this cleanly — it tells the browser what characters to use for open-quote and close-quote values. No more manual escaping. No more mixing guillemets and curly quotes on the same site. This is the property you reach for when your design requires French, German, or Japanese quotation conventions without littering the markup with « and »`.

What the `quotes` Property Actually Does

The quotes property defines what characters the browser should insert when it encounters the CSS-generated content values open-quote and close-quote. You've probably used content: open-quote; inside ::before to automatically insert a quotation mark. Without quotes, the browser uses its default — usually the language-appropriate characters based on the element's lang attribute. That's fine for simple English text. But when you need specific typographic style (curly quotes, guillemets, corner brackets) or when you're supporting multiple languages on the same page, defaults won't cut it.

You set it like this: quotes: '“' '”' '‘' '’';. The first two values are the outermost open/close quote; the next two are for first-level nested quotes. You can add as many pairs as you need, though most designs never exceed three nesting levels.

styles.cssCSS
1
2
3
4
5
6
7
8
9
10
11
12
/* Set custom curly quotes for English */
.english-quote {
  quotes: '“' '”' '‘' '’';
}

.english-quote::before {
  content: open-quote;
}

.english-quote::after {
  content: close-quote;
}
Think of quotes as a stack of pairs
  • Level 1 (outermost): uses pair index 0 — open/close
Production Insight
Browsers treat quotes as a list of character pairs, _not_ a single string.
If you accidentally write quotes: '"' '"' (only one pair), nested quotes will reuse the same pair — never alternate.
Always define at least two pairs: one for outer, one for first nesting level.
Key Takeaway
The quotes property is a list of character pairs, consumed level by level.
If you define only one pair, nested quotes will reuse it.
Always write at least two pairs for proper nesting behaviour.
When to use custom quotes vs. auto
IfSingle language, standard glyphs are fine
UseUse no custom quotes — let the browser default apply
IfNeed curly/smart quotes instead of straight quotes
UseSet quotes: '“' '”' '‘' '’'; explicitly
IfMulti-lingual site (e.g., English + French)
UseUse :lang() selectors to switch quotes per language
IfCustom typographic style (e.g., diamond brackets)
UseSet quotes to any characters you want — no limits

Language-Specific Quotation Marks via `:lang()`

Different languages have distinct quotation conventions. French uses guillemets (« »), German uses „low double“ and “high double” („“), Japanese uses corner brackets (「 」). The simplest way to handle this is to define the quotations in CSS rules scoped to the :lang() pseudo-class. This works hand-in-hand with the lang attribute you put on <html> or section elements.

When the browser sees open-quote, it checks the element's language. If a matching quotes rule is found via :lang(en), it uses that. Otherwise, it falls back to the element's own quotes value (if set) or the default. The key is to set :lang(X) selectors for every language your site supports — otherwise your French pages may render English-style quotes.

global-quotes.cssCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* English */
:lang(en) {
  quotes: '“' '”' '‘' '’';
}

/* French */
:lang(fr) {
  quotes: '«' '»' '‹' '›';
}

/* German (low-high style) */
:lang(de) {
  quotes: '„' '“' '‚' '‘';
}

/* Japanese */
:lang(ja) {
  quotes: '「' '」' '『' '』';
}

/* In your markup */
<div lang="fr">
  <q>Je pense, donc je suis</q>
</div>
The `lang` attribute must be set
Without a lang attribute on the element or an ancestor, the browser cannot infer the language. The :lang() selector will not match, and the default (usually browser-locale-based) will apply. Set lang on <html> for the whole page, then override on specific elements when needed.
Production Insight
A common failure: the <html> tag has no lang attribute, or it's set programmatically but too late.
Quotes will default to the browser's UI language, which is often English.
Always hardcode lang="en" (or your primary language) in the server-rendered HTML.
Key Takeaway
Use :lang() selectors to switch quotes per language.
The lang attribute must be present for browsers to match the rule.
This is the production-standard way to handle multi-lingual quotation marks.
Setting up multi-language quotes
IfSite has 2–3 languages, quotes differ
UseOne :lang() rule per language, each with its own quotes list
IfSite uses a single language but custom style
UseOne rule on the container element, no need for :lang()
IfContent is dynamically loaded in varied languages
UseSet lang attribute on each piece of content via the API; CSS will pick it up

Nested Quotes: How the Browser Alternates Pairs

When you have a quotation inside another quotation (e.g., a quote within a blockquote), the browser automatically alternates the quotation marks. It uses the pairs defined in quotes in order: first pair for outermost, second pair for first nesting level, third for second nesting, etc. If there are more nesting levels than pairs, the browser cycles back to the last pair.

This behavior is automatic — you don't need to track nesting level in your CSS. The browser counts how many open-quote calls are active (without matching close-quote) and picks the appropriate pair. That means your CSS can be completely declarative: you just define the pairs, and the browser handles the rest.

For five or more levels of nesting (rare in practice), you should define enough pairs to cover the deepest expected nesting. If you define only one pair, all nesting levels will use the same characters, which looks wrong.

nested-quotes.cssCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Define three pairs for deep nesting */
blockquote {
  quotes: '“' '”' '‘' '’' '‹' '›';
}

blockquote::before {
  content: open-quote;
}

blockquote::after {
  content: close-quote;
}

/* HTML example */
<blockquote>
  She said,
  <blockquote>
    He whispered,
    <blockquote>
      The final clue
    </blockquote>
  </blockquote>
</blockquote>
Production Insight
Deeply nested blockquotes (3+ levels) are rare in most content, but they happen in legal documents and academic papers.
If you only define two pairs, the third nesting level repeats the last pair — which may look fine or may be confusing.
Define at least as many pairs as your deepest expected nesting, then add one more for safety.
Key Takeaway
Browsers match nesting levels to quotes pairs in order.
Cycles happen if levels exceed defined pairs — it's safe but may not match your design.
Always think about the deepest possible nesting in your content.

The `open-quote` and `close-quote` Pseudo-Elements

The quotes property is meaningless on its own — it only takes effect when an element uses the content property with the values open-quote or close-quote. These are typically used in ::before and ::after pseudo-elements to surround inline quotes or blockquote decorations.

Most implementations put open-quote on ::before and close-quote on ::after. That's the idiomatic approach. However, you can place them anywhere — even on actual elements (using content: open-quote on an element directly, though that requires the element to be a replaced element or have a certain layout; less common). The key is consistency: the browser keeps a stack of open quotes for each element tree. When you issue close-quote, it closes the most recent unclosed open-quote in that scope.

This stacking mechanism is why you can nest quotes correctly without JavaScript: the browser implicitly tracks the depth.

pseudo-elements.cssCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Standard pattern - before for open, after for close */
q {
  quotes: '“' '”' '‘' '’';
}

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

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

/* Inline usage */
<p>The philosopher said <q>Cogito, <q>ergo sum</q> I think</q>, therefore I am.</p>
Production Insight
Never hardcode quote characters in pseudo-elements' content property.
That breaks nesting and makes your CSS language-dependent.
Always use content: open-quote — the quotes property will supply the right character.
Key Takeaway
open-quote and close-quote are the values you must use in content.
Do not hardcode characters — let quotes handle the symbol selection.
The browser's quote stack ensures correct nesting automatically.

Custom Quotes Beyond Standard Typography

The quotes property accepts any Unicode characters, not just traditional quotation marks. You can use emoji, symbols, or even repeated characters to create decorative borders. For example, you could use >> and <<, or custom bracket shapes. The only limitation is that the characters must be representable in the document's encoding (UTF-8 recommended).

This is useful for theming: imagine a fantasy website that uses star symbols (★☆) as quote markers. Or a code documentation site that uses backtick-like characters. The quotes property makes this possible without touching the HTML.

custom-symbols.cssCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Decorative star quotes */
blockquote.star-quote {
  quotes: '★' '☆' '✦' '✧';
}

/* Arrow-themed quotes */
blockquote.arrow-quote {
  quotes: '▶' '◀' '▷' '◁';
}

/* Use them in pseudo-elements */
blockquote.star-quote::before {
  content: open-quote;
}
blockquote.star-quote::after {
  content: close-quote;
}
Production Insight
Emoji characters inside quotes are supported, but they may affect line height and spacing.
Test across browsers — some may render emoji at different sizes.
For production, stick to typographic characters unless you have a compelling design reason.
Key Takeaway
quotes can use any Unicode character — emoji, arrows, stars.
Great for theming, but test character rendering across browsers.
Standard quotation marks are safest for content-heavy sites.
● Production incidentPOST-MORTEMseverity: high

Chinese quotes disappeared after a CMS upgrade

Symptom
All blockquote elements lost their quotation marks in the Chinese locale, while the English side still showed standard double quotes.
Assumption
"The CMS must have stripped our quote entities." The team checked HTML and confirmed entities were still present.
Root cause
A global stylesheet had been minified and a rule containing quotes: '\201C' '\201D' '\2018' '\2019'; was dropped because the minifier saw it as invalid — the actual Unicode escapes were correct but the original author used a wrong backslash pattern that broke after minification.
Fix
Replace the escaped Unicode with the actual characters (U+201C, U+201D) wrapped in spaces: `quotes: '“' '”' '‘' '’';`. Then verify the minifier preserves them (upgrade to a CSS-aware minifier that handles Unicode properly).
Key lesson
  • Never rely on Unicode escapes inside CSS strings — use the real characters directly.
  • Always verify `quotes` render correctly after any CSS pipeline change (minifier, postcss, purgecss).
  • Add a visual regression test that checks quotation marks are present in every locale.
Production debug guideSymptom → Action table for common `quotes` failures4 entries
Symptom · 01
Quotation marks vanish on all elements
Fix
Inspect computed styles – `quotes is inheritable but must be set on a parent. Check if a CSS reset like normalize.css sets quotes: none`.
Symptom · 02
Nested quotes use the same style (e.g., double quotes inside double quotes)
Fix
Verify the quotes property defines at least two pairs: the first for outermost, second for first nesting level. Add more pairs for deeper nesting.
Symptom · 03
Specific language (e.g., French) shows wrong characters
Fix
Check if a `:lang selector overrides the quotes property. Use :lang(fr) { quotes: '«' '»' '‹' '›'; }`.
Symptom · 04
Quotation marks appear but are misaligned / oversized
Fix
The quote characters themselves have typographic metrics. If the font doesn't include those glyphs, the browser falls back – check `font-family` for proper coverage.
★ Quick Debug: CSS QuotesWhen quotation marks behave unexpectedly, run these checks in order.
No quotes anywhere
Immediate action
Check if any rule overrides `quotes` to `none` or the initial value
Commands
DevTools > Computed > filter 'quotes' — confirm property value
Check stylesheets for `quotes: none` in resets or base styles
Fix now
Add quotes: auto; (modern browsers) or explicit pairs to the container
Wrong quote characters for language+
Immediate action
Inspect the element and check `:lang()` selectors
Commands
DevTools > Styles > check if a `:lang` rule is applying `quotes`
Add `:lang(fr) { quotes: '«' '»' '‹' '›'; }` and test
Fix now
Ensure the lang attribute is set on <html> or the relevant element
Nested quotes show same character (no alternation)+
Immediate action
Count pairs in the `quotes` value – need at least two
Commands
Get computed style: `getComputedStyle(element).getPropertyValue('quotes')`
If only one pair, add more: `quotes: '""' '""' '``' '``';`
Fix now
Add a second pair; if more than two levels of nesting expected, add a third pair
CSS Quotes Property vs Hardcoded Entities
AspectCSS `quotes` propertyHardcoded HTML entities
NestingAutomatic – browser tracks depthManual – must insert correct entity per level
Language switchingTrivial via :lang() selectorsRequires server-side logic or JS
MaintenanceOne CSS change updates everywhereFind & replace over entire markup
Performance overhead0 – computed at render time0 – but larger HTML payload
AccessibilityScreen readers may not read generated contentEntities are read as characters

Key takeaways

1
The quotes property is a list of character pairs, consumed level by level for nested quotes.
2
Always define at least two pairs to handle single-level nesting correctly.
3
Use :lang() selectors for multi-lingual sites; ensure the lang attribute is present.
4
Let `content
open-quote` handle insertion — never hardcode quote characters in pseudo-elements.
5
Custom characters (emoji, symbols) work but require testing across browsers and font support.
6
The quote stack is per-element-tree, so nested <q> or <blockquote> elements work automatically.

Common mistakes to avoid

4 patterns
×

Forgetting to set `quotes` before using `open-quote`

Symptom
Quotes appear using browser default (often the wrong language style) or no quotes at all.
Fix
Always define quotes on the element or a parent before using content: open-quote/close-quote. Use quotes: auto as a quick fallback.
×

Using only one pair of quotes in the `quotes` property

Symptom
Nested blockquotes show the same quote character at both levels, e.g., double quotes inside double quotes.
Fix
Add at least two pairs: quotes: '“' '”' '‘' '’';. Add a third if deep nesting is expected.
×

Hardcoding quote characters in `::before`/`::after` content

Symptom
Nesting breaks — the hardcoded character doesn't change on nested elements.
Fix
Replace hardcoded characters with content: open-quote; and content: close-quote; and manage styling via the quotes property.
×

Not setting `lang` attribute on the `<html>` element

Symptom
Language-specific :lang() rules never match; quotes fall back to browser defaults.
Fix
Set lang="en" (or appropriate) on the <html> tag immediately, and override with lang attribute on containers when content language differs.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
How does the CSS `quotes` property interact with the `content` property?
Q02SENIOR
What happens when nesting exceeds the number of pairs defined in the `qu...
Q03SENIOR
How would you implement French quotation marks (guillemets) only for ele...
Q01 of 03JUNIOR

How does the CSS `quotes` property interact with the `content` property?

ANSWER
The quotes property defines character pairs that are inserted when you use content: open-quote and content: close-quote in pseudo-elements (::before, ::after). The browser maintains a stack of open quotes per element tree. When it encounters open-quote, it pushes the current level onto the stack and outputs the corresponding open character from the pair. close-quote pops the stack and outputs the matching close character. This ensures correct nesting without JavaScript.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Does the `quotes` property affect screen readers?
02
Can I use the `quotes` property with inline quotes (``)?
03
Does `quotes` inherit?
04
What if I want no quotation marks?
🔥

That's HTML & CSS. Mark it forged?

3 min read · try the examples if you haven't

Previous
HTML iframe: Embedding External Content Explained
14 / 16 · HTML & CSS
Next
Bootstrap Accordion: Collapsible Sections with Plus/Minus Toggle