Senior 8 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 & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
May 23, 2026
last updated
1,663
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
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
✦ Definition~90s read
What is CSS Quotes?

CSS quotes is a property that controls which quotation marks the browser inserts when you use the open-quote and close-quote values in the content property of ::before and ::after pseudo-elements. It exists to solve a real problem: hardcoding curly quotes like “ and ” in your HTML is fragile, breaks when you change languages, and makes translation files a nightmare.

Imagine you run a restaurant with locations in Paris, Tokyo, and New York.

Instead, quotes lets you define a stack of quote pairs (e.g., '“' '”' '‘' '’'), and the browser automatically alternates between them for nested <q> elements or manual pseudo-element injections. The :lang() pseudo-class can even swap the entire stack per language — so English gets “ ”, French gets « », and German gets „ “ — all without touching your markup.

This is not a toy; it’s how you build typographically correct, localizable UIs without littering your templates with Unicode escapes or entity references.

Where this property fits in the ecosystem is as a CSS-level alternative to hardcoding quote characters in HTML or JavaScript. The <q> element already uses quotes by default, but most developers ignore it and just type “ or &ldquo; directly. That works until you need to support Japanese, Swedish, or Polish quotation conventions — then you’re either duplicating templates or writing JS to swap characters. quotes with :lang() handles that declaratively.

But here’s the production trap: minifiers like Terser, esbuild, or CSSNano often mangle Unicode escapes inside content values, turning '\201C' into garbage or stripping the backslash entirely. If you define quotes with escaped Unicode (e.g., '\201C' '\201D'), a minifier may collapse the escape into the literal character, which then gets double-escaped or lost when the CSS is served compressed.

The result is broken quotation marks in production that work fine in dev. You’re better off using the actual Unicode characters directly in your CSS source (e.g., '“' '”') and relying on your build tool’s charset handling — or you’ll spend hours debugging why your French quotes show as empty boxes after deployment.

When not to use quotes: if your content is static and single-language, hardcoding the characters is simpler and more predictable. If you’re generating quotes dynamically from a CMS or API, the property won’t help — you still need the backend to emit the correct characters.

And if your build pipeline aggressively minifies CSS (common in Next.js, Vite, or webpack setups), test your quotes declarations with the actual minifier, not just in dev. The property is powerful for design systems and i18n-heavy apps, but it’s not a silver bullet — it’s a CSS feature that demands you understand how your toolchain handles Unicode.

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 &laquo; and &raquo;`.

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
CSS Quotes: Unicode Escapes & Minifier Pitfalls THECODEFORGE.IO CSS Quotes: Unicode Escapes & Minifier Pitfalls Flow from property definition to production trap with nested quotes quotes Property Defines pairs of quotation marks per language :lang() Selector Applies language-specific quote pairs Nested Quotes Browser alternates between quote pairs open-quote / close-quote Pseudo-elements insert quotes from property Custom Quotes Override with any Unicode characters Minifier Trap Unicode escapes broken by minification ⚠ Minifiers mangle Unicode escapes in quotes values Use CSS escapes or avoid minification on quotes THECODEFORGE.IO
thecodeforge.io
CSS Quotes: Unicode Escapes & Minifier Pitfalls
Css Quotes

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.

The Production Trap: Why `quotes` Won’t Save Your Translation Files

You've memorized the quotes property syntax. Congratulations. Now go delete it from your i18n pipeline. Relying on CSS for quotation marks creates a brittle coupling between your presentation layer and your content layer that breaks the moment you need to localize quotes dynamically.

The core problem: CSS quotes is a purely visual property. It doesn't touch the DOM. When your backend returns a string like He said, "Hello", and you apply quotes: '«' '»', the browser swaps the ASCII double-quotes for «Hello». That works—until your translator hands you a CDATA block with actual Unicode quotation marks embedded in the source. Now your CSS and the raw content fight each other, producing double-quoted gibberish.

Worse, any JavaScript that reads innerText or processes the string for analytics will see the raw ASCII quotes, not the styled version. You've created a two-faced API: pretty in the browser, ugly in every other context.

Real solution: Handle quotation marks at the template level or in your translation middleware. Let the backend or a build-time transform insert the correct Unicode characters (like “ and ”) based on locale. CSS quotes is fine for static, single-language brochures. For dynamic apps, it’s a liability.

localeQuotes.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — javascript tutorial

// Correct: transform at render time, not in CSS
const localeMap = {
  en: { open: '\u201C', close: '\u201D' },
  fr: { open: '\u00AB\u00A0', close: '\u00A0\u00BB' },
  de: { open: '\u201E', close: '\u201C' }
};

function wrapQuotes(text, locale = 'en') {
  const q = localeMap[locale] || localeMap['en'];
  return `${q.open}${text}${q.close}`;
}

// Usage in a React component
const Quote = ({ text, locale }) => (
  <blockquote>{wrapQuotes(text, locale)}</blockquote>
);
Output
// For locale='fr', text='Bonjour'
'« Bonjour »'
// For locale='de', text='Hallo'
'„Hallo“'
// DOM remains semantic, analytics see Unicode
Production Trap:
Never concatenate CSS quotes with server-rendered content that already contains quote entities. You'll get double rendering: '„„Hello““'. Instead, strip ASCII quotes in the JS layer before applying locale rules.
Key Takeaway
CSS quotes is a presentational band-aid. For multi-locale apps, handles quotes in your template/render layer, not in your stylesheet.

Debugging Nested Quotes: When Your Blockquote Looks Like a Typing Disaster

You've got a blockquote inside a blockquote inside a blockquote. Your client's lawyer is emailing you screenshots of what looks like a prison tattoo: '"‘"'. Calm down. The browser's default quotes property only defines four levels of nesting. After that, it cycles back to the first level. That's why nested quotes degrades into an unreadable mess.

Here's what happens: The HTML spec defines a default quote stack for each language. For English, it's “ ” (level 1), then ‘ ’ (level 2), then back to level 1 for level 3. If you have three levels of <q> or blockquote, level 3 looks identical to level 1. Your reader can't tell who is quoting whom.

Fix: Override the quotes property with a custom stack that provides unique symbols for up to six levels. I use this pattern on documentation-heavy sites: - Level 1: “ ” (double quotes) - Level 2: ‘ ’ (single quotes) - Level 3: « » (guillemets, visually distinct) - Level 4: „ “ (low double, inverted)

Then enforce a lint rule that limits blockquote nesting to 4 levels in your markdown parser. Your designers will thank you. Your support team will stop filing bugs about 'random curly braces.'

deepNestQuotes.cssJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* io.thecodeforge — javascript tutorial */

/* Four-level quote stack for deep nesting */
blockquote, q {
  quotes:
    '\201C' '\201D'  /* level 1: “ ” */
    '\2018' '\2019'  /* level 2: ‘ ’ */
    '\00AB' '\00BB'  /* level 3: « » */
    '\201E' '\201C'; /* level 4: „ “ */
}

/* CSS will cycle back to level 1 after level 4.
   To prevent infinite recursion, cap nesting
   in your component tree. */
Output
// Rendered HTML:
<blockquote>
Level 1: “Outer quote”
<blockquote>
Level 2: ‘Inner quote’
<blockquote>
Level 3: «Deeper still»
<blockquote>
Level 4: „Deepest“
<blockquote>
Level 5: “Cycles back” /* identical to level 1 */
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
Senior Shortcut:
Use a preprocessor or PostCSS plugin to generate your quotes stack from a config file. Then you can update locales globally without touching CSS. Also, add a max-nesting-depth to your stylelint config to prevent more than 4 nested blockquotes.
Key Takeaway
Default CSS quote nesting cycles after 2 levels. Define a 4-level custom stack and enforce a nesting cap in your component logic to keep quote hierarchies readable.

Why `quotes` Doesn’t Respect the `content` Property Like You Think

Junior devs think setting quotes in CSS magically injects quotation marks everywhere. It doesn’t. The property only defines the character pairs. You still need the content property with open-quote or close-quote to actually render them. That’s why you can hunt through your stylesheet, see quotes defined, and wonder why nothing shows up.

The real enemy? Someone forgot to add content: open-quote to the ::before pseudo-element. Or worse, they used content: "“" directly, hardcoding the character instead of using the semantic value. That breaks nested alternation and language switching.

Production fix: Always pair quotes with content on ::before and ::after. If you’re using a CMS that strips pseudo-elements, your quotes will vanish silently. Audit your build pipeline for that.

CSS-QuoteContentBug.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — javascript tutorial

// This CSS will define quotes but NEVER render them:
.q {
  quotes: "“" "”" "‘" "’";
}

// Because there's no pseudo-element with content
// Expected: 
// <span class="q">Hello</span> → “Hello”
// Actual: Hello (no quotes)

// Fix: add ::before and ::after
.q::before {
  content: open-quote;
}
.q::after {
  content: close-quote;
}
Output
Hello → “Hello” after fix
Production Trap:
If your CSS framework resets pseudo-elements (like Tailwind’s @layer base), your custom quotes will be ignored. Always declare your content in the same specificity level.
Key Takeaway
Declaring quotes without content on ::before/::after is dead code. No content = no quotes.

The `no-open-quote` Hack That Saves Nested Messes

When you have three levels of nested blockquotes, the browser alternates between the first and second quote pair. That’s fine for English. Real problems start when your design calls for a single quote mark at a specific level, but CSS keeps alternating and gives you the wrong character.

Solution: Use no-open-quote as a content value. It increments the quote nesting counter without rendering a character. This forces the next open-quote to use the correct pair from your list, skipping a level.

Why bother? Because production translations break. German uses „“ but English uses “”. If you’re serving multilingual content, no-open-quote lets you reset mid-nest without touching JavaScript. No runtime cost. No localization bugs.

Test this: apply content: no-open-quote on the deep nested element’s ::before. The browser skips one quote generation. Suddenly your quote marks align with your translation file. Single line fix, no framework overhead.

NoOpenQuoteFix.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — javascript tutorial

// Problem: three nested quotes always alternates
blockquote q::before {
  content: open-quote;
}

// Nested level 3 gets the second pair (curly quotes)
// You want it to get the first pair again

// Fix: add no-open-quote
.level3::before {
  content: no-open-quote;
}
// Now next open-quote uses first pair again

// HTML: <q>outer <q>mid <q class="level3">inner</q></q></q>
// Before fix: “ ‘ ‘inner’ ” → wrong quote marks for inner
// After fix: “ ‘ “inner” ’ ” → correct first pair on inner
Output
“ ‘ “inner” ’ ”
Senior Shortcut:
Use no-open-quote as a debug tool. When nested quotes look wrong, add it to the deepest ::before and watch the pattern correct itself. Easier than rewriting your quotes property.
Key Takeaway
no-open-quote manipulates the quote counter without rendering. Use it to skip pairs and match language-specific quoting conventions.

Formal Definition: What the Spec Actually Says

The CSS quotes property is formally defined in the CSS Generated Content Module Level 3. Its initial value is auto, meaning the browser uses language-specific quotation marks from the user agent stylesheet. When set explicitly, the value is a space-separated list of string pairs: [<string> <string>]+. Each pair defines an opening and closing quote. The first pair is for the outermost quote level, the second for the first nesting, and so on. If you provide fewer pairs than nesting levels, the browser repeats the final pair. The property applies to all elements but only takes effect when content uses open-quote or close-quote values. Common values include none (no quotes generated) and custom pairs like '\201C' '\201D' '\2018' '\2019' for curly English quotes. Failing to match the pair count to your nesting depth produces undefined rendering across browsers.

quotesDefinition.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// io.thecodeforge — javascript tutorial

// Retrieve computed quotes for debugging
function getComputedQuotes(element) {
  return getComputedStyle(element).quotes;
}

const blockquote = document.querySelector('blockquote');
console.log(getComputedQuotes(blockquote)); // 'auto' or explicit pairs

// Warn if quotes are 'auto' and nesting exceeds 1
if (getComputedQuotes(blockquote) === 'auto') {
  console.warn('Using auto: browser defaults may mismatch nesting.');
}

// Check for insufficient pairs
const quoteValue = getComputedQuotes(blockquote);
if (quoteValue !== 'auto' && quoteValue !== 'none') {
  const pairs = quoteValue.split(' ').length / 2;
  if (pairs < 2) {
    console.warn('Only', pairs, 'quote pair(s) defined. Deep nesting repeats last pair.');
  }
}
Output
auto or explicit pair string
Production Trap:
Browsers differ on how they handle auto in edge cases. Safari may ignore auto on <q> inside shadow DOM. Always test across engines.
Key Takeaway
The quotes property is a list of string pairs; missing pairs repeat the last one, causing unintended quote styles in deep nesting.

CSS Styling: Making Quotes Look Intentional

The quotes property alone does not style quotation marks—it only defines their character content. To make quotes visually distinct, use CSS font properties, color, and spacing on the generated content. Target the ::before and ::after pseudo-elements that browsers create for open-quote and close-quote. For example, assign a larger font-size, a distinct color, or font-weight to make opening quotes stand out. You can also add margin-right or margin-left to separate the quote character from the text. Combine with content: open-quote and content: close-quote in custom pseudo-elements for fine-grained control. Avoid using padding on the pseudo-element itself—it shifts the text layout. Instead, apply margin to the quote character. For blockquotes, consider adding a vertical bar or background color via the pseudo-element, independent of the quote marks. This separation of quote definition from quote styling prevents layout regressions when quotes change per locale.

styleQuotes.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// io.thecodeforge — javascript tutorial

// Apply custom styling to quote pseudo-elements
const style = document.createElement('style');
style.textContent = `
  blockquote::before {
    content: open-quote;
    font-size: 2em;
    color: #2c3e50;
    margin-right: 0.15em;
    font-family: Georgia, serif;
  }
  blockquote::after {
    content: close-quote;
    font-size: 2em;
    color: #2c3e50;
    margin-left: 0.15em;
    vertical-align: super;
  }
  blockquote {
    quotes: '\201C' '\201D' '\2018' '\2019';
  }
`;
document.head.appendChild(style);

// Verify styles applied
const q = document.querySelector('blockquote');
console.log('Quotes:', getComputedStyle(q).quotes);
console.log('Pseudo color:', getComputedStyle(q, '::before').color);
Output
Quotes: '\201C' '\201D' '\2018' '\2019'
Pseudo color: rgb(44, 62, 80)
Production Trap:
Setting font-size on ::before shifts the quote baseline relative to adjacent inline text. Use vertical-align or line-height adjustments to correct it.
Key Takeaway
Style quote pseudo-elements with font and spacing properties, not the quotes property itself, which only defines character content.
● 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?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
May 23, 2026
last updated
1,663
articles · all by Naren
🔥

That's HTML & CSS. Mark it forged?

8 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