Skip to content

[Vite 8 regression] Bundled CSS with non-ASCII codepoints (icon fonts) misrenders due to missing charset declaration #22381

@alphathekiwi

Description

@alphathekiwi

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:

  1. Match esbuild's behavior and emit non-ASCII codepoints as escapes (e.g. \f200), making the bundle encoding-independent. This was Vite 6 behavior.
  2. Emit @charset \"UTF-8\"; at the top of any CSS bundle containing non-ASCII bytes.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions