Skip to content

fix(css): preserve CSS modules linked from HTML in build#22259

Open
semimikoh wants to merge 1 commit intovitejs:mainfrom
semimikoh:fix/html-css-module-build
Open

fix(css): preserve CSS modules linked from HTML in build#22259
semimikoh wants to merge 1 commit intovitejs:mainfrom
semimikoh:fix/html-css-module-build

Conversation

@semimikoh
Copy link
Copy Markdown
Contributor

@semimikoh semimikoh commented Apr 18, 2026

Description

Fixes a dev/build inconsistency where CSS Modules referenced directly from HTML with <link rel="stylesheet"> were applied in dev but
could be dropped from production builds.

In build, HTML stylesheet links are converted into JS imports. For CSS Modules, that meant the imported module produced JS exports and
was marked side-effect-free, so Rollup could tree-shake the import when the exports were unused. The CSS was then not collected into the
final CSS asset.

This change marks CSS Modules imported from HTML stylesheet links with an internal ?html-style query. The CSS still goes through the
normal CSS Modules/PostCSS pipeline, but the generated JS module is empty and kept as a side-effect import so the processed CSS is
included in the build output.

Changes

  • Add an internal ?html-style marker for CSS Modules linked from HTML stylesheets.
  • Keep ?html-style CSS Modules from being tree-shaken during build.
  • Treat HTML stylesheet CSS Modules as pure CSS chunks instead of JS-bearing CSS Module chunks.
  • Add a playground regression covering <link rel="stylesheet" href="./html-linked.module.css">.

Tests

  • git diff --check
  • pnpm run test-build playground/css

Notes

pnpm run test-serve playground/css hung without output in the local environment and was terminated. The regression is build-specific
and is covered by the passing build playground test.

Fixes #22242

@sapphi-red
Copy link
Copy Markdown
Member

Why did it work in Vite v7 without this change?

@semimikoh
Copy link
Copy Markdown
Contributor Author

It likely worked in Vite v7 because this edge case wasn't exposed in the previous build pipeline the same way.

The underlying issue is that a CSS Module linked from HTML is converted into a JS import during build, even though from HTML we only need its CSS side effect. In Vite v8, CSS Modules that generate JS exports can be treated as tree-shakeable, so an HTML-linked CSS Module may be removed when those exports are unused.

So I wouldn't frame this as "v7 handled it correctly and v8 regressed because of one specific change" so much as: the HTML-linked CSS Module case was ambiguous before, and Vite v8 made that ambiguity observable. This PR makes that intent explicit by treating those imports as CSS side-effect imports instead.

@sapphi-red
Copy link
Copy Markdown
Member

In Vite v8, CSS Modules that generate JS exports can be treated as tree-shakeable

Why were it not treated as tree-shakeable in v7? But is in v8.

@semimikoh
Copy link
Copy Markdown
Contributor Author

I don't think this is best explained as "v7 always preserved CSS Modules and #16051 changed that", since the CSS Modules moduleSideEffects handling already existed before. The issue here is specifically that HTML-linked CSS Modules are converted into JS imports during build, even though from HTML we only need their CSS side effect. In the Vite v8 build pipeline, that makes this edge case observable because those imports can be treated like normal CSS Module JS imports and removed when their exports are unused.

This PR makes that explicit by treating HTML-linked CSS Modules as CSS side-effect imports instead of normal CSS Module JS imports.

@sapphi-red
Copy link
Copy Markdown
Member

What is the concrete change that makes this observable in v8? If moduleSideEffects: false on CSS Modules and the HTML <link> to JS import conversion both already existed in v7, that alone should have been enough to tree-shake HTML-linked CSS Modules in v7 too. It didn't, so something else must have changed.

Without knowing what that change is, I'm not sure this is the right place to fix it. It might belong closer to whatever actually changed (e.g. how we emit the JS module, or something else in the CSS/HTML pipeline).

@semimikoh
Copy link
Copy Markdown
Contributor Author

That’s fair. I don’t think I’ve pinned down the concrete change with enough confidence yet.

What I have confirmed so far is the Vite-side structure around this path, but not the exact behavioral difference that made it become observable only in v8. I’ll dig into that more before asserting that this is definitely the right layer to fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Vite 8] Inconsistencies in CSS Module loading from HTML file

2 participants