Skip to content

@tailwindcss/vite: Duplicate unlayered preflight emitted in production build with Vite Environment API (multi-environment bundling) #19853

@zdqsgithub

Description

@zdqsgithub

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.2
  • tailwindcss@4.2.2
  • vite@8.0.1
  • vinext@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:

  1. Run npx vite build
  2. Inspect the output CSS file in dist/client/assets/index-*.css
  3. 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 @layer blocks close

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:

  1. Deduplicate preflight output when processing multiple Vite environments, or
  2. Ensure all emitted preflight CSS is always wrapped in @layer base, regardless of how the final bundle is assembled

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions