Problem
@extend from postcss-extend-rule is silently discarded in CSS Modules. Every declaration the rule should have inherited is missing — in dev and in the production build alike — with no warning or error.
/* styles.module.css */
.shared { position: absolute; visibility: hidden; }
.wrap { @extend .shared; border: 1px solid red; }
Expected: .wrap inherits position: absolute; visibility: hidden.
Actual output:
._shared_1u9uh_1{visibility:hidden;position:absolute}
._wrap_1u9uh_2{border:1px solid red}
The at-rule is gone and nothing was copied.
Cause
Vite's compileCSS puts the CSS-Modules plugin at the front of the PostCSS chain:
if (isModule) postcssPlugins.unshift((await importPostcssModules()).default({ ... }))
So class names are already hashed to ._shared_1u9uh_1 by the time the project's postcss-extend-rule runs. @extend .shared matches no rule, and the plugin drops it silently.
Running the same PostCSS config standalone over the same file resolves the extend correctly, so the configuration is fine — only the ordering inside Vite's pipeline is wrong.
Reproduction (no vinext involved)
postcss.config.mjs -> export default { plugins: { 'postcss-extend-rule': {} } }
src/styles.module.css -> the two rules above
src/main.js -> import s from "./styles.module.css"
vite build
The emitted CSS shows _wrap_ without the extended declarations. This reproduces on plain Vite with no vinext in the project, so the underlying issue is Vite's plugin ordering — filing here because vinext already resolves and injects the project's PostCSS config, and is where a Next-compatible pipeline would be expected to handle it (webpack/css-loader runs the user's PostCSS before CSS-Modules scoping, so the same stylesheet works under next build).
Impact
Any CSS module using @extend loses those declarations. In one Pages Router app this silently affected 46 @extend uses across 11 module files, producing components with missing positioning — dropdown panels rendering in flow instead of as absolutely-positioned overlays, etc. Because nothing warns, it presents as "the CSS is just wrong" with no obvious cause.
Workaround
Expanding @extend before Vite's pipeline sees the file, via an enforce: "pre" transform, fixes it — the extend resolves against the original selectors and Vite then hashes the already-expanded rules:
const expandCssModuleExtends = () => {
let processor;
return {
name: "expand-css-module-extends",
enforce: "pre" as const,
async transform(code: string, id: string) {
const file = id.split("?")[0];
if (!file.endsWith(".module.css") || !code.includes("@extend")) return null;
if (!processor) {
const postcss = (await import("postcss")).default;
const extendRule = (await import("postcss-extend-rule")).default;
const presetEnv = (await import("postcss-preset-env")).default;
// `@extend` must see flat selectors: postcss-extend-rule skips nested
// rules whose selector it cannot resolve, so flatten nesting first.
processor = postcss([
presetEnv({ stage: false, features: { "nesting-rules": true } }),
extendRule(),
]);
}
const result = await processor.process(code, { from: file });
return { code: result.css, map: null };
},
};
};
Note the nesting step — without it only the @extends at one nesting depth are expanded, which looks like a partial fix.
Environment
- vinext
1.0.0-beta.6
- Vite
8.1.0
postcss-extend-rule, postcss-preset-env
- Reproduced in dev and in the production build, and on plain Vite without vinext
Problem
@extendfrompostcss-extend-ruleis silently discarded in CSS Modules. Every declaration the rule should have inherited is missing — in dev and in the production build alike — with no warning or error.Expected:
.wrapinheritsposition: absolute; visibility: hidden.Actual output:
The at-rule is gone and nothing was copied.
Cause
Vite's
compileCSSputs the CSS-Modules plugin at the front of the PostCSS chain:So class names are already hashed to
._shared_1u9uh_1by the time the project'spostcss-extend-ruleruns.@extend .sharedmatches no rule, and the plugin drops it silently.Running the same PostCSS config standalone over the same file resolves the extend correctly, so the configuration is fine — only the ordering inside Vite's pipeline is wrong.
Reproduction (no vinext involved)
The emitted CSS shows
_wrap_without the extended declarations. This reproduces on plain Vite with no vinext in the project, so the underlying issue is Vite's plugin ordering — filing here because vinext already resolves and injects the project's PostCSS config, and is where a Next-compatible pipeline would be expected to handle it (webpack/css-loaderruns the user's PostCSS before CSS-Modules scoping, so the same stylesheet works undernext build).Impact
Any CSS module using
@extendloses those declarations. In one Pages Router app this silently affected 46@extenduses across 11 module files, producing components with missing positioning — dropdown panels rendering in flow instead of as absolutely-positioned overlays, etc. Because nothing warns, it presents as "the CSS is just wrong" with no obvious cause.Workaround
Expanding
@extendbefore Vite's pipeline sees the file, via anenforce: "pre"transform, fixes it — the extend resolves against the original selectors and Vite then hashes the already-expanded rules:Note the nesting step — without it only the
@extends at one nesting depth are expanded, which looks like a partial fix.Environment
1.0.0-beta.68.1.0postcss-extend-rule,postcss-preset-env