-
Notifications
You must be signed in to change notification settings - Fork 544
Description
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
- Create a standard Inertia v3 React app with SSR enabled via the
@inertiajs/viteplugin - In
resources/css/app.css:
@import 'tailwindcss';
@theme {
--font-sans: 'MyCustomFont', ui-sans-serif, system-ui, sans-serif;
}- 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} />);
},
});- Run
bun run dev(Vite dev server) - 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">- 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).