-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Description
Description
When using @tailwindcss/vite with Vite 6's Environment API (via @cloudflare/vite-plugin + vinext), the production build emits a duplicate universal preflight reset (*,:before,:after{margin:0;padding:0}) outside any @layer block at the end of the CSS bundle.
Since unlayered CSS always beats @layer rules in the cascade, this causes all Tailwind spacing utility classes (mt-*, pt-*, px-*, py-*, gap-*, etc.) to resolve to 0px in production, while the Vite dev server works correctly.
Reproduction
Versions:
@tailwindcss/vite@4.2.2tailwindcss@4.2.2vite@8.0.1vinext@0.0.35@cloudflare/vite-plugin@1.30.0
vite.config.ts:
import { defineConfig } from "vite";
import vinext from "vinext";
import { cloudflare } from "@cloudflare/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [
tailwindcss(),
vinext(),
cloudflare({
viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] },
}),
],
});globals.css:
@import "tailwindcss";Steps to reproduce:
- Run
npx vite build - Inspect the output CSS file in
dist/client/assets/index-*.css - Search for
*,:before,:after{— you'll find two occurrences:- One correctly inside
@layer base { ... }near the beginning (~position 10200) - One unlayered at the end of the file (~position 125000), after all
@layerblocks close
- One correctly inside
Expected behavior:
The preflight reset should appear exactly once, inside @layer base.
Actual behavior:
A second copy is emitted unlayered at the end of the CSS bundle. This unlayered copy overrides all @layer utilities rules in the CSS cascade, breaking every spacing utility.
Root Cause Analysis
The Cloudflare Vite plugin defines multiple Vite environments (rsc + ssr + client). Each environment independently triggers @tailwindcss/vite's CSS processing pipeline, generating its own copy of the Tailwind output including preflight. When the final client CSS bundle is assembled, the duplicate preflight from a secondary environment (likely SSR) is concatenated outside the @layer base wrapper of the primary environment's output.
The dev server is not affected because it injects styles via <style> tags with proper HMR layer ordering.
Impact
Every margin and padding utility class resolves to 0px in production:
| Utility | Dev Server | Production Build |
|---|---|---|
mt-20 (margin-top) |
80px |
0px |
pt-24 (padding-top) |
96px |
0px |
px-8 (padding-x) |
32px |
0px |
gap-8 (gap) |
32px |
0px |
| Font size, color, display | Works | Works |
Workaround
We added a custom Vite plugin that strips the duplicate in generateBundle:
function stripDuplicatePreflight() {
return {
name: 'strip-duplicate-preflight',
enforce: 'post' as const,
generateBundle(_options: unknown, bundle: Record<string, { type: string; source?: string }>) {
for (const [fileName, chunk] of Object.entries(bundle)) {
if (fileName.endsWith('.css') && chunk.type === 'asset' && typeof chunk.source === 'string') {
const pattern = /\*\s*,\s*:+before\s*,\s*:+after\s*\{[^}]*margin:0[^}]*padding:0[^}]*\}/g;
let lastMatch: RegExpExecArray | null = null;
let match;
while ((match = pattern.exec(chunk.source)) !== null) {
lastMatch = match;
}
if (lastMatch && lastMatch.index > chunk.source.length / 2) {
const before = chunk.source.substring(0, lastMatch.index);
const after = chunk.source.substring(lastMatch.index + lastMatch[0].length);
chunk.source = before + after;
}
}
}
}
}
}Suggested Fix
@tailwindcss/vite should either:
- Deduplicate preflight output when processing multiple Vite environments, or
- Ensure all emitted preflight CSS is always wrapped in
@layer base, regardless of how the final bundle is assembled