CSS Quotes — Why Minifiers Break Unicode Escapes
Minifiers silently drop CSS quotes rules with backslash escapes, causing quotation marks to vanish by locale.
- 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
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.
- Level 1 (outermost): uses pair index 0 — open/close
quotes as a list of character pairs, _not_ a single string.quotes: '"' '"' (only one pair), nested quotes will reuse the same pair — never alternate.quotes property is a list of character pairs, consumed level by level.quotes — let the browser default applyquotes: '“' '”' '‘' '’'; explicitly:lang() selectors to switch quotes per languagequotes to any characters you want — no limitsLanguage-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.
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.<html> tag has no lang attribute, or it's set programmatically but too late.lang="en" (or your primary language) in the server-rendered HTML.:lang() selectors to switch quotes per language.lang attribute must be present for browsers to match the rule.:lang() rule per language, each with its own quotes list:lang()lang attribute on each piece of content via the API; CSS will pick it upNested 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.
quotes pairs in order.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.
content property.content: open-quote — the quotes property will supply the right character.open-quote and close-quote are the values you must use in content.quotes handle the symbol selection.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.
quotes are supported, but they may affect line height and spacing.quotes can use any Unicode character — emoji, arrows, stars.Chinese quotes disappeared after a CMS upgrade
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.quotes: '“' '”' '‘' '’';`. Then verify the minifier preserves them (upgrade to a CSS-aware minifier that handles Unicode properly).- 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.
quotes is inheritable but must be set on a parent. Check if a CSS reset like normalize.css sets quotes: none`.quotes property defines at least two pairs: the first for outermost, second for first nesting level. Add more pairs for deeper nesting.:lang selector overrides the quotes property. Use :lang(fr) { quotes: '«' '»' '‹' '›'; }`.font-family` for proper coverage.quotes: auto; (modern browsers) or explicit pairs to the containerKey takeaways
quotes property is a list of character pairs, consumed level by level for nested quotes.:lang() selectors for multi-lingual sites; ensure the lang attribute is present.<q> or <blockquote> elements work automatically.Common mistakes to avoid
4 patternsForgetting to set `quotes` before using `open-quote`
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
quotes: '“' '”' '‘' '’';. Add a third if deep nesting is expected.Hardcoding quote characters in `::before`/`::after` content
content: open-quote; and content: close-quote; and manage styling via the quotes property.Not setting `lang` attribute on the `<html>` element
:lang() rules never match; quotes fall back to browser defaults.lang="en" (or appropriate) on the <html> tag immediately, and override with lang attribute on containers when content language differs.Interview Questions on This Topic
How does the CSS `quotes` property interact with the `content` property?
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.Frequently Asked Questions
That's HTML & CSS. Mark it forged?
3 min read · try the examples if you haven't