Describe the bug
In Vite 8, CSS bundles containing non-ASCII codepoints (e.g. PUA codepoints in content: rules used by icon fonts like video-react) emit raw UTF-8 bytes with no @charset declaration, no BOM, and no charset=utf-8 in the HTTP Content-Type.
When the browser has no charset signal it can fall back to a non-UTF-8 environment encoding, which decodes a 3-byte UTF-8 codepoint (e.g. 0xEF 0x88 0x80 → U+F200) as three Windows-1252 characters (). Icon fonts then render fallback .notdef glyphs.
This worked in Vite 6 because esbuild (then the default CSS minifier) defaults to charset: "ascii", escaping non-ASCII as \f200 so the bundle was pure ASCII and encoding-independent.
Reproduction
Bundle a project that imports a CSS file containing literal PUA characters in a content rule (e.g. video-react's CSS, older FontAwesome versions, many icon fonts):
.icon-play:before { content: ""; /* literal U+F200 between the quotes */ }
The Vite 8 output bundle preserves the literal UTF-8 bytes and emits no charset markers. Serving that file as Content-Type: text/css (no charset) causes the bug.
Concretely, in DevTools on the deployed app:
// Bundle on disk has the correct codepoint:
const css = await (await fetch('/assets/index-XXXX.css')).text();
const m = css.match(/icon-play:before[^}]*content:"([^"]+)"/);
[...m[1]].map(c => c.codePointAt(0).toString(16))
// → ["f200"] ✓ length 1, single PUA codepoint
// But the browser-parsed CSS decodes it as three Windows-1252 chars:
const el = document.querySelector('.icon-play');
getComputedStyle(el, ':before').content
// → '""' ✗ three chars, mis-decoded UTF-8 bytes
// And there is no @charset or BOM in the bundle:
css.includes('@charset') // false
// And the response Content-Type is `text/css` (no charset).
Expected behavior
Either:
- Match esbuild's behavior and emit non-ASCII codepoints as escapes (e.g.
\f200), making the bundle encoding-independent. This was Vite 6 behavior.
- Emit
@charset \"UTF-8\"; at the top of any CSS bundle containing non-ASCII bytes.
- Emit a UTF-8 BOM at the start of CSS bundles.
System Info
- Vite 8.0.10
@vitejs/plugin-react 6.0.1
- No
lightningcss config (default rolldown CSS minifier)
- Reproduced on Chrome 131 / macOS
Workaround
A small Vite plugin that prepends @charset \"UTF-8\"; to all bundled CSS files in generateBundle resolves the issue.
Describe the bug
In Vite 8, CSS bundles containing non-ASCII codepoints (e.g. PUA codepoints in
content:rules used by icon fonts likevideo-react) emit raw UTF-8 bytes with no@charsetdeclaration, no BOM, and nocharset=utf-8in the HTTPContent-Type.When the browser has no charset signal it can fall back to a non-UTF-8 environment encoding, which decodes a 3-byte UTF-8 codepoint (e.g.
0xEF 0x88 0x80→ U+F200) as three Windows-1252 characters (). Icon fonts then render fallback.notdefglyphs.This worked in Vite 6 because esbuild (then the default CSS minifier) defaults to
charset: "ascii", escaping non-ASCII as\f200so the bundle was pure ASCII and encoding-independent.Reproduction
Bundle a project that imports a CSS file containing literal PUA characters in a
contentrule (e.g.video-react's CSS, older FontAwesome versions, many icon fonts):The Vite 8 output bundle preserves the literal UTF-8 bytes and emits no charset markers. Serving that file as
Content-Type: text/css(no charset) causes the bug.Concretely, in DevTools on the deployed app:
Expected behavior
Either:
\f200), making the bundle encoding-independent. This was Vite 6 behavior.@charset \"UTF-8\";at the top of any CSS bundle containing non-ASCII bytes.System Info
@vitejs/plugin-react6.0.1lightningcssconfig (default rolldown CSS minifier)Workaround
A small Vite plugin that prepends
@charset \"UTF-8\";to all bundled CSS files ingenerateBundleresolves the issue.