Skip to content

SSR CSS collection injects duplicate sub-dependency stylesheets, overriding Tailwind @theme customizations #2935

@entity

Description

@entity

Inertia version

3.x (beta)

Inertia adapter(s) affected

  • React
  • Vue 3
  • Svelte
  • Not Applicable

JS package version

3.0.0-beta.2

Backend stack (optional)

Laravel 12
PHP 8.4
inertiajs/inertia-laravel v2
Tailwind CSS 4.2.1
@tailwindcss/vite 4.2.1
Vite 7.3.1

Describe the problem

Hey all! I've created a branch for my app to try out Inertia.js v3, so far it's been great! However I noticed a weird CSS issue. I threw this to Claude and it said this was an Inertia bug, so thought I'd throw its output here! Hope it helps


During SSR in dev mode, collectCSSFromModuleGraph in @inertiajs/vite walks the SSR module graph and injects <link> tags for every CSS module it finds — including transitive sub-dependencies of CSS files (i.e. CSS files imported via @import inside other CSS files).

This is problematic because CSS-processing Vite plugins like @tailwindcss/vite already compile @import sub-dependencies into the parent stylesheet. Injecting them again as separate <link> tags causes duplicate, conflicting CSS.

For example, when app.css contains @import 'tailwindcss' and a @theme block with custom overrides, @tailwindcss/vite compiles both together — the output of app.css contains the correct custom values. But the SSR CSS collector also finds tailwindcss/index.css in the module graph and injects it as a separate <link> tag. This separate stylesheet contains the default Tailwind theme values and loads after app.css, overriding the custom @theme values (e.g. custom --font-sans is lost).

Expected: Only root CSS files (directly imported by JS modules) should be injected as tags. CSS @import sub-dependencies are already compiled into their parent by their respective Vite plugins.

Actual: All transitive CSS dependencies are injected, causing duplicate stylesheets with conflicting values.

Steps to reproduce

  1. Create a standard Inertia v3 React app with SSR enabled via the @inertiajs/vite plugin
  2. In resources/css/app.css:
@import 'tailwindcss';


@theme {
  --font-sans: 'MyCustomFont', ui-sans-serif, system-ui, sans-serif;
}
  1. In resources/js/app.tsx:
import '../css/app.css';
import { createInertiaApp } from '@inertiajs/react';
import { hydrateRoot } from 'react-dom/client';

createInertiaApp({
  resolve: name => {
    const pages = import.meta.glob('./pages/**/*.tsx');
    return pages[`./pages/${name}.tsx`]();
  },
  setup({ el, App, props }) {
    hydrateRoot(el, <App {...props} />);
  },
});
  1. Run bun run dev (Vite dev server)
  2. Load any page and inspect the HTML — you'll see both:
<link rel="stylesheet" href="...app.css" data-vite-dev-id="...">
<link rel="stylesheet" href="...tailwindcss/index.css">
  1. Check the computed value:
// Returns the Tailwind DEFAULT, not 'MyCustomFont'
getComputedStyle(document.documentElement).getPropertyValue('--font-sans')

Workaround: Split @import 'tailwindcss' into individual imports and pre-import tailwindcss/theme.css from JavaScript before app.css, so it's collected first in the depth-first module graph traversal and app.css loads after it (winning the cascade).

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