From e231ba15266b33c4432f33abdcebe759955a386a Mon Sep 17 00:00:00 2001
From: Manuel Schiller <manuel.schiller@caligano.de>
Date: Sat, 10 Jan 2026 03:44:06 +0100
Subject: [PATCH 1/3] feat: collect styles in dev

fixes #3023
---
 e2e/react-start/css-modules/.gitignore        |   7 +
 e2e/react-start/css-modules/.prettierignore   |   1 +
 e2e/react-start/css-modules/package.json      |  34 ++++
 .../css-modules/playwright.config.ts          |  40 ++++
 e2e/react-start/css-modules/src/router.tsx    |  12 ++
 .../css-modules/src/routes/__root.tsx         |  60 ++++++
 .../css-modules/src/routes/index.tsx          |  25 +++
 .../css-modules/src/routes/modules.tsx        |  27 +++
 .../css-modules/src/styles/card.module.css    |  20 ++
 .../css-modules/src/styles/global.css         |  19 ++
 e2e/react-start/css-modules/tests/css.spec.ts | 137 ++++++++++++++
 .../css-modules/tests/setup/global.setup.ts   |   6 +
 .../tests/setup/global.teardown.ts            |   6 +
 e2e/react-start/css-modules/tsconfig.json     |  22 +++
 e2e/react-start/css-modules/vite.config.ts    |  17 ++
 e2e/solid-start/css-modules/.gitignore        |   7 +
 e2e/solid-start/css-modules/.prettierignore   |   1 +
 e2e/solid-start/css-modules/package.json      |  31 +++
 .../css-modules/playwright.config.ts          |  40 ++++
 e2e/solid-start/css-modules/src/router.tsx    |  12 ++
 .../css-modules/src/routes/__root.tsx         |  62 ++++++
 .../css-modules/src/routes/index.tsx          |  25 +++
 .../css-modules/src/routes/modules.tsx        |  27 +++
 .../css-modules/src/styles/card.module.css    |  20 ++
 .../css-modules/src/styles/global.css         |  19 ++
 e2e/solid-start/css-modules/tests/css.spec.ts | 137 ++++++++++++++
 .../css-modules/tests/setup/global.setup.ts   |   6 +
 .../tests/setup/global.teardown.ts            |   6 +
 e2e/solid-start/css-modules/tsconfig.json     |  24 +++
 e2e/solid-start/css-modules/vite.config.ts    |  17 ++
 e2e/vue-start/css-modules/.gitignore          |   7 +
 e2e/vue-start/css-modules/.prettierignore     |   1 +
 e2e/vue-start/css-modules/package.json        |  31 +++
 .../css-modules/playwright.config.ts          |  40 ++++
 e2e/vue-start/css-modules/src/router.tsx      |  12 ++
 .../css-modules/src/routes/__root.tsx         |  62 ++++++
 .../css-modules/src/routes/index.tsx          |  25 +++
 .../css-modules/src/routes/modules.tsx        |  27 +++
 .../css-modules/src/styles/card.module.css    |  20 ++
 .../css-modules/src/styles/global.css         |  19 ++
 e2e/vue-start/css-modules/tests/css.spec.ts   | 137 ++++++++++++++
 .../css-modules/tests/setup/global.setup.ts   |   6 +
 .../tests/setup/global.teardown.ts            |   6 +
 e2e/vue-start/css-modules/tsconfig.json       |  24 +++
 e2e/vue-start/css-modules/vite.config.ts      |  17 ++
 packages/react-router/src/HeadContent.tsx     |  44 ++++-
 packages/solid-router/src/HeadContent.tsx     |  28 ++-
 .../src/dev-server-plugin/dev-styles.ts       | 176 ++++++++++++++++++
 .../src/dev-server-plugin/plugin.ts           |  51 ++++-
 packages/vue-router/src/HeadContent.tsx       |  44 ++++-
 pnpm-lock.yaml                                | 144 ++++++++++++--
 51 files changed, 1769 insertions(+), 19 deletions(-)
 create mode 100644 e2e/react-start/css-modules/.gitignore
 create mode 100644 e2e/react-start/css-modules/.prettierignore
 create mode 100644 e2e/react-start/css-modules/package.json
 create mode 100644 e2e/react-start/css-modules/playwright.config.ts
 create mode 100644 e2e/react-start/css-modules/src/router.tsx
 create mode 100644 e2e/react-start/css-modules/src/routes/__root.tsx
 create mode 100644 e2e/react-start/css-modules/src/routes/index.tsx
 create mode 100644 e2e/react-start/css-modules/src/routes/modules.tsx
 create mode 100644 e2e/react-start/css-modules/src/styles/card.module.css
 create mode 100644 e2e/react-start/css-modules/src/styles/global.css
 create mode 100644 e2e/react-start/css-modules/tests/css.spec.ts
 create mode 100644 e2e/react-start/css-modules/tests/setup/global.setup.ts
 create mode 100644 e2e/react-start/css-modules/tests/setup/global.teardown.ts
 create mode 100644 e2e/react-start/css-modules/tsconfig.json
 create mode 100644 e2e/react-start/css-modules/vite.config.ts
 create mode 100644 e2e/solid-start/css-modules/.gitignore
 create mode 100644 e2e/solid-start/css-modules/.prettierignore
 create mode 100644 e2e/solid-start/css-modules/package.json
 create mode 100644 e2e/solid-start/css-modules/playwright.config.ts
 create mode 100644 e2e/solid-start/css-modules/src/router.tsx
 create mode 100644 e2e/solid-start/css-modules/src/routes/__root.tsx
 create mode 100644 e2e/solid-start/css-modules/src/routes/index.tsx
 create mode 100644 e2e/solid-start/css-modules/src/routes/modules.tsx
 create mode 100644 e2e/solid-start/css-modules/src/styles/card.module.css
 create mode 100644 e2e/solid-start/css-modules/src/styles/global.css
 create mode 100644 e2e/solid-start/css-modules/tests/css.spec.ts
 create mode 100644 e2e/solid-start/css-modules/tests/setup/global.setup.ts
 create mode 100644 e2e/solid-start/css-modules/tests/setup/global.teardown.ts
 create mode 100644 e2e/solid-start/css-modules/tsconfig.json
 create mode 100644 e2e/solid-start/css-modules/vite.config.ts
 create mode 100644 e2e/vue-start/css-modules/.gitignore
 create mode 100644 e2e/vue-start/css-modules/.prettierignore
 create mode 100644 e2e/vue-start/css-modules/package.json
 create mode 100644 e2e/vue-start/css-modules/playwright.config.ts
 create mode 100644 e2e/vue-start/css-modules/src/router.tsx
 create mode 100644 e2e/vue-start/css-modules/src/routes/__root.tsx
 create mode 100644 e2e/vue-start/css-modules/src/routes/index.tsx
 create mode 100644 e2e/vue-start/css-modules/src/routes/modules.tsx
 create mode 100644 e2e/vue-start/css-modules/src/styles/card.module.css
 create mode 100644 e2e/vue-start/css-modules/src/styles/global.css
 create mode 100644 e2e/vue-start/css-modules/tests/css.spec.ts
 create mode 100644 e2e/vue-start/css-modules/tests/setup/global.setup.ts
 create mode 100644 e2e/vue-start/css-modules/tests/setup/global.teardown.ts
 create mode 100644 e2e/vue-start/css-modules/tsconfig.json
 create mode 100644 e2e/vue-start/css-modules/vite.config.ts
 create mode 100644 packages/start-plugin-core/src/dev-server-plugin/dev-styles.ts

diff --git a/e2e/react-start/css-modules/.gitignore b/e2e/react-start/css-modules/.gitignore
new file mode 100644
index 0000000000..950326609c
--- /dev/null
+++ b/e2e/react-start/css-modules/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+dist
+.routeTree.gen.ts
+src/routeTree.gen.ts
+test-results
+playwright-report
+port*.txt
diff --git a/e2e/react-start/css-modules/.prettierignore b/e2e/react-start/css-modules/.prettierignore
new file mode 100644
index 0000000000..083bdb7c4c
--- /dev/null
+++ b/e2e/react-start/css-modules/.prettierignore
@@ -0,0 +1 @@
+src/routeTree.gen.ts
diff --git a/e2e/react-start/css-modules/package.json b/e2e/react-start/css-modules/package.json
new file mode 100644
index 0000000000..a52c3872ed
--- /dev/null
+++ b/e2e/react-start/css-modules/package.json
@@ -0,0 +1,34 @@
+{
+  "name": "tanstack-react-start-e2e-css-modules",
+  "private": true,
+  "sideEffects": false,
+  "type": "module",
+  "scripts": {
+    "dev": "vite dev --port 3000",
+    "dev:e2e": "vite dev --port $PORT",
+    "build": "vite build && tsc --noEmit",
+    "preview": "vite preview",
+    "start": "pnpx srvx --prod -s ../client dist/server/server.js",
+    "test:e2e:dev": "MODE=dev playwright test --project=chromium",
+    "test:e2e:prod": "playwright test --project=chromium",
+    "test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev"
+  },
+  "dependencies": {
+    "@tanstack/react-router": "workspace:^",
+    "@tanstack/react-start": "workspace:^",
+    "react": "^19.0.0",
+    "react-dom": "^19.0.0"
+  },
+  "devDependencies": {
+    "@playwright/test": "^1.50.1",
+    "@tanstack/router-e2e-utils": "workspace:^",
+    "@types/node": "^22.10.2",
+    "@types/react": "^19.0.8",
+    "@types/react-dom": "^19.0.3",
+    "@vitejs/plugin-react": "^4.3.4",
+    "srvx": "^0.10.0",
+    "typescript": "^5.7.2",
+    "vite": "^7.1.7",
+    "vite-tsconfig-paths": "^5.1.4"
+  }
+}
diff --git a/e2e/react-start/css-modules/playwright.config.ts b/e2e/react-start/css-modules/playwright.config.ts
new file mode 100644
index 0000000000..7582bbbb53
--- /dev/null
+++ b/e2e/react-start/css-modules/playwright.config.ts
@@ -0,0 +1,40 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const isDev = process.env.MODE === 'dev'
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+export default defineConfig({
+  testDir: './tests',
+  workers: 1,
+  reporter: [['line']],
+
+  globalSetup: './tests/setup/global.setup.ts',
+  globalTeardown: './tests/setup/global.teardown.ts',
+
+  use: {
+    baseURL,
+  },
+
+  webServer: {
+    command: isDev ? `pnpm dev:e2e` : `pnpm build && PORT=${PORT} pnpm start`,
+    url: baseURL,
+    reuseExistingServer: !process.env.CI,
+    stdout: 'pipe',
+    env: {
+      VITE_NODE_ENV: 'test',
+      PORT: String(PORT),
+    },
+  },
+
+  projects: [
+    {
+      name: 'chromium',
+      use: {
+        ...devices['Desktop Chrome'],
+      },
+    },
+  ],
+})
diff --git a/e2e/react-start/css-modules/src/router.tsx b/e2e/react-start/css-modules/src/router.tsx
new file mode 100644
index 0000000000..83ea998b39
--- /dev/null
+++ b/e2e/react-start/css-modules/src/router.tsx
@@ -0,0 +1,12 @@
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+  const router = createRouter({
+    routeTree,
+    scrollRestoration: true,
+    defaultPreload: false,
+  })
+
+  return router
+}
diff --git a/e2e/react-start/css-modules/src/routes/__root.tsx b/e2e/react-start/css-modules/src/routes/__root.tsx
new file mode 100644
index 0000000000..d44c33b8d0
--- /dev/null
+++ b/e2e/react-start/css-modules/src/routes/__root.tsx
@@ -0,0 +1,60 @@
+import {
+  HeadContent,
+  Link,
+  Outlet,
+  Scripts,
+  createRootRoute,
+} from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+  head: () => ({
+    meta: [
+      { charSet: 'utf-8' },
+      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+    ],
+  }),
+  component: RootComponent,
+})
+
+function RootComponent() {
+  return (
+    <html>
+      <head>
+        <HeadContent />
+      </head>
+      <body>
+        <nav
+          data-testid="main-nav"
+          style={{
+            padding: '12px 20px',
+            borderBottom: '1px solid #e2e8f0',
+            display: 'flex',
+            alignItems: 'center',
+            gap: '16px',
+            backgroundColor: '#fff',
+          }}
+        >
+          <Link
+            to="/"
+            style={{ color: '#0284c7', textDecoration: 'none' }}
+            data-testid="nav-home"
+          >
+            Home (Global CSS)
+          </Link>
+          <Link
+            to="/modules"
+            style={{ color: '#0284c7', textDecoration: 'none' }}
+            data-testid="nav-modules"
+          >
+            CSS Modules
+          </Link>
+        </nav>
+
+        <main style={{ padding: '20px' }}>
+          <Outlet />
+        </main>
+        <Scripts />
+      </body>
+    </html>
+  )
+}
diff --git a/e2e/react-start/css-modules/src/routes/index.tsx b/e2e/react-start/css-modules/src/routes/index.tsx
new file mode 100644
index 0000000000..f7668802f1
--- /dev/null
+++ b/e2e/react-start/css-modules/src/routes/index.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute } from '@tanstack/react-router'
+import '~/styles/global.css'
+
+export const Route = createFileRoute('/')({
+  component: Home,
+})
+
+function Home() {
+  return (
+    <div>
+      <h1>CSS Collection Test - Global CSS</h1>
+      <p>This page tests that global CSS is collected and served during SSR.</p>
+
+      <div className="global-container" data-testid="global-styled">
+        <div className="global-title" data-testid="global-title">
+          Global CSS Applied
+        </div>
+        <div className="global-description" data-testid="global-description">
+          This container should have a blue background, white text, and rounded
+          corners even with JavaScript disabled.
+        </div>
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/react-start/css-modules/src/routes/modules.tsx b/e2e/react-start/css-modules/src/routes/modules.tsx
new file mode 100644
index 0000000000..101a612b8b
--- /dev/null
+++ b/e2e/react-start/css-modules/src/routes/modules.tsx
@@ -0,0 +1,27 @@
+import { createFileRoute } from '@tanstack/react-router'
+import styles from '~/styles/card.module.css'
+
+export const Route = createFileRoute('/modules')({
+  component: Modules,
+})
+
+function Modules() {
+  return (
+    <div>
+      <h1>CSS Collection Test - CSS Modules</h1>
+      <p>
+        This page tests that CSS modules are collected and served during SSR.
+      </p>
+
+      <div className={styles.card} data-testid="module-card">
+        <div className={styles.title} data-testid="module-title">
+          CSS Module Applied
+        </div>
+        <div className={styles.content} data-testid="module-content">
+          This card should have a green theme with scoped class names even with
+          JavaScript disabled.
+        </div>
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/react-start/css-modules/src/styles/card.module.css b/e2e/react-start/css-modules/src/styles/card.module.css
new file mode 100644
index 0000000000..fcbcb419aa
--- /dev/null
+++ b/e2e/react-start/css-modules/src/styles/card.module.css
@@ -0,0 +1,20 @@
+/* CSS Module for testing scoped styles in dev mode */
+
+.card {
+  background-color: #f0fdf4; /* green-50 */
+  padding: 16px;
+  border-radius: 8px;
+  border: 1px solid #22c55e; /* green-500 */
+}
+
+.title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #166534; /* green-800 */
+  margin-bottom: 8px;
+}
+
+.content {
+  font-size: 14px;
+  color: #15803d; /* green-700 */
+}
diff --git a/e2e/react-start/css-modules/src/styles/global.css b/e2e/react-start/css-modules/src/styles/global.css
new file mode 100644
index 0000000000..5bef746780
--- /dev/null
+++ b/e2e/react-start/css-modules/src/styles/global.css
@@ -0,0 +1,19 @@
+/* Global styles for testing CSS collection in dev mode */
+
+.global-container {
+  background-color: #3b82f6; /* blue-500 */
+  padding: 24px;
+  border-radius: 12px;
+  color: white;
+}
+
+.global-title {
+  font-size: 24px;
+  font-weight: bold;
+  margin-bottom: 16px;
+}
+
+.global-description {
+  font-size: 16px;
+  opacity: 0.9;
+}
diff --git a/e2e/react-start/css-modules/tests/css.spec.ts b/e2e/react-start/css-modules/tests/css.spec.ts
new file mode 100644
index 0000000000..ec4955f9b2
--- /dev/null
+++ b/e2e/react-start/css-modules/tests/css.spec.ts
@@ -0,0 +1,137 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+test.describe('CSS styles in SSR (dev mode)', () => {
+  test.describe('with JavaScript disabled', () => {
+    test.use({ javaScriptEnabled: false })
+
+    test('global CSS is applied on initial page load', async ({ page }) => {
+      await page.goto('/')
+
+      const element = page.getByTestId('global-styled')
+      await expect(element).toBeVisible()
+
+      // Verify the CSS is applied by checking computed styles
+      // #3b82f6 (blue-500) in RGB is rgb(59, 130, 246)
+      const backgroundColor = await element.evaluate(
+        (el) => getComputedStyle(el).backgroundColor,
+      )
+      expect(backgroundColor).toBe('rgb(59, 130, 246)')
+
+      const padding = await element.evaluate(
+        (el) => getComputedStyle(el).padding,
+      )
+      expect(padding).toBe('24px')
+
+      const borderRadius = await element.evaluate(
+        (el) => getComputedStyle(el).borderRadius,
+      )
+      expect(borderRadius).toBe('12px')
+    })
+
+    test('CSS modules are applied on initial page load', async ({ page }) => {
+      await page.goto('/modules')
+
+      const card = page.getByTestId('module-card')
+      await expect(card).toBeVisible()
+
+      // Verify class is scoped (hashed)
+      const className = await card.getAttribute('class')
+      expect(className).toBeTruthy()
+      expect(className).not.toBe('card')
+      // The class should contain some hash characters (CSS modules add a hash)
+      expect(className!.length).toBeGreaterThan(5)
+
+      // Verify computed styles from card.module.css
+      // #f0fdf4 (green-50) in RGB is rgb(240, 253, 244)
+      const backgroundColor = await card.evaluate(
+        (el) => getComputedStyle(el).backgroundColor,
+      )
+      expect(backgroundColor).toBe('rgb(240, 253, 244)')
+
+      const padding = await card.evaluate((el) => getComputedStyle(el).padding)
+      expect(padding).toBe('16px')
+
+      const borderRadius = await card.evaluate(
+        (el) => getComputedStyle(el).borderRadius,
+      )
+      expect(borderRadius).toBe('8px')
+    })
+
+    test('global CSS class names are NOT scoped', async ({ page }) => {
+      await page.goto('/')
+
+      const element = page.getByTestId('global-styled')
+      await expect(element).toBeVisible()
+
+      // Get the class attribute - it should be the plain class name (not hashed)
+      const className = await element.getAttribute('class')
+      expect(className).toBe('global-container')
+    })
+  })
+
+  test('styles persist after hydration', async ({ page }) => {
+    await page.goto('/')
+
+    // Wait for hydration
+    await page.waitForTimeout(1000)
+
+    const element = page.getByTestId('global-styled')
+    const backgroundColor = await element.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(59, 130, 246)')
+  })
+
+  test('CSS modules styles persist after hydration', async ({ page }) => {
+    await page.goto('/modules')
+
+    // Wait for hydration
+    await page.waitForTimeout(1000)
+
+    const card = page.getByTestId('module-card')
+    const backgroundColor = await card.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(240, 253, 244)')
+  })
+
+  test('styles work correctly after client-side navigation', async ({
+    page,
+  }) => {
+    // Start from home
+    await page.goto('/')
+    await page.waitForTimeout(1000)
+
+    // Verify initial styles
+    const globalElement = page.getByTestId('global-styled')
+    await expect(globalElement).toBeVisible()
+    let backgroundColor = await globalElement.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(59, 130, 246)')
+
+    // Navigate to modules page
+    await page.getByTestId('nav-modules').click()
+    await page.waitForURL('/modules')
+
+    // Verify CSS modules styles
+    const card = page.getByTestId('module-card')
+    await expect(card).toBeVisible()
+    backgroundColor = await card.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(240, 253, 244)')
+
+    // Navigate back to home
+    await page.getByTestId('nav-home').click()
+    await page.waitForURL('/')
+
+    // Verify global styles still work
+    await expect(globalElement).toBeVisible()
+    backgroundColor = await globalElement.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(59, 130, 246)')
+  })
+})
diff --git a/e2e/react-start/css-modules/tests/setup/global.setup.ts b/e2e/react-start/css-modules/tests/setup/global.setup.ts
new file mode 100644
index 0000000000..3593d10ab9
--- /dev/null
+++ b/e2e/react-start/css-modules/tests/setup/global.setup.ts
@@ -0,0 +1,6 @@
+import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function setup() {
+  await e2eStartDummyServer(packageJson.name)
+}
diff --git a/e2e/react-start/css-modules/tests/setup/global.teardown.ts b/e2e/react-start/css-modules/tests/setup/global.teardown.ts
new file mode 100644
index 0000000000..62fd79911c
--- /dev/null
+++ b/e2e/react-start/css-modules/tests/setup/global.teardown.ts
@@ -0,0 +1,6 @@
+import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function teardown() {
+  await e2eStopDummyServer(packageJson.name)
+}
diff --git a/e2e/react-start/css-modules/tsconfig.json b/e2e/react-start/css-modules/tsconfig.json
new file mode 100644
index 0000000000..3a9fb7cd71
--- /dev/null
+++ b/e2e/react-start/css-modules/tsconfig.json
@@ -0,0 +1,22 @@
+{
+  "include": ["**/*.ts", "**/*.tsx"],
+  "compilerOptions": {
+    "strict": true,
+    "esModuleInterop": true,
+    "jsx": "react-jsx",
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "lib": ["DOM", "DOM.Iterable", "ES2022"],
+    "isolatedModules": true,
+    "resolveJsonModule": true,
+    "skipLibCheck": true,
+    "target": "ES2022",
+    "allowJs": true,
+    "forceConsistentCasingInFileNames": true,
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["./src/*"]
+    },
+    "noEmit": true
+  }
+}
diff --git a/e2e/react-start/css-modules/vite.config.ts b/e2e/react-start/css-modules/vite.config.ts
new file mode 100644
index 0000000000..c2c28ae93b
--- /dev/null
+++ b/e2e/react-start/css-modules/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import viteReact from '@vitejs/plugin-react'
+
+export default defineConfig({
+  server: {
+    port: 3000,
+  },
+  plugins: [
+    tsConfigPaths({
+      projects: ['./tsconfig.json'],
+    }),
+    tanstackStart(),
+    viteReact(),
+  ],
+})
diff --git a/e2e/solid-start/css-modules/.gitignore b/e2e/solid-start/css-modules/.gitignore
new file mode 100644
index 0000000000..950326609c
--- /dev/null
+++ b/e2e/solid-start/css-modules/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+dist
+.routeTree.gen.ts
+src/routeTree.gen.ts
+test-results
+playwright-report
+port*.txt
diff --git a/e2e/solid-start/css-modules/.prettierignore b/e2e/solid-start/css-modules/.prettierignore
new file mode 100644
index 0000000000..083bdb7c4c
--- /dev/null
+++ b/e2e/solid-start/css-modules/.prettierignore
@@ -0,0 +1 @@
+src/routeTree.gen.ts
diff --git a/e2e/solid-start/css-modules/package.json b/e2e/solid-start/css-modules/package.json
new file mode 100644
index 0000000000..27417100bf
--- /dev/null
+++ b/e2e/solid-start/css-modules/package.json
@@ -0,0 +1,31 @@
+{
+  "name": "tanstack-solid-start-e2e-css-modules",
+  "private": true,
+  "sideEffects": false,
+  "type": "module",
+  "scripts": {
+    "dev": "vite dev --port 3000",
+    "dev:e2e": "vite dev --port $PORT",
+    "build": "vite build && tsc --noEmit",
+    "preview": "vite preview",
+    "start": "pnpx srvx --prod -s ../client dist/server/server.js",
+    "test:e2e:dev": "MODE=dev playwright test --project=chromium",
+    "test:e2e:prod": "playwright test --project=chromium",
+    "test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev"
+  },
+  "dependencies": {
+    "@tanstack/solid-router": "workspace:^",
+    "@tanstack/solid-start": "workspace:^",
+    "solid-js": "^1.9.10"
+  },
+  "devDependencies": {
+    "@playwright/test": "^1.50.1",
+    "@tanstack/router-e2e-utils": "workspace:^",
+    "@types/node": "^22.10.2",
+    "srvx": "^0.10.0",
+    "typescript": "^5.7.2",
+    "vite": "^7.1.7",
+    "vite-plugin-solid": "^2.11.10",
+    "vite-tsconfig-paths": "^5.1.4"
+  }
+}
diff --git a/e2e/solid-start/css-modules/playwright.config.ts b/e2e/solid-start/css-modules/playwright.config.ts
new file mode 100644
index 0000000000..7582bbbb53
--- /dev/null
+++ b/e2e/solid-start/css-modules/playwright.config.ts
@@ -0,0 +1,40 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const isDev = process.env.MODE === 'dev'
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+export default defineConfig({
+  testDir: './tests',
+  workers: 1,
+  reporter: [['line']],
+
+  globalSetup: './tests/setup/global.setup.ts',
+  globalTeardown: './tests/setup/global.teardown.ts',
+
+  use: {
+    baseURL,
+  },
+
+  webServer: {
+    command: isDev ? `pnpm dev:e2e` : `pnpm build && PORT=${PORT} pnpm start`,
+    url: baseURL,
+    reuseExistingServer: !process.env.CI,
+    stdout: 'pipe',
+    env: {
+      VITE_NODE_ENV: 'test',
+      PORT: String(PORT),
+    },
+  },
+
+  projects: [
+    {
+      name: 'chromium',
+      use: {
+        ...devices['Desktop Chrome'],
+      },
+    },
+  ],
+})
diff --git a/e2e/solid-start/css-modules/src/router.tsx b/e2e/solid-start/css-modules/src/router.tsx
new file mode 100644
index 0000000000..aa5762b4f7
--- /dev/null
+++ b/e2e/solid-start/css-modules/src/router.tsx
@@ -0,0 +1,12 @@
+import { createRouter } from '@tanstack/solid-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+  const router = createRouter({
+    routeTree,
+    scrollRestoration: true,
+    defaultPreload: false,
+  })
+
+  return router
+}
diff --git a/e2e/solid-start/css-modules/src/routes/__root.tsx b/e2e/solid-start/css-modules/src/routes/__root.tsx
new file mode 100644
index 0000000000..304d76fb33
--- /dev/null
+++ b/e2e/solid-start/css-modules/src/routes/__root.tsx
@@ -0,0 +1,62 @@
+import {
+  HeadContent,
+  Link,
+  Outlet,
+  Scripts,
+  createRootRoute,
+} from '@tanstack/solid-router'
+import { HydrationScript } from 'solid-js/web'
+
+export const Route = createRootRoute({
+  head: () => ({
+    meta: [
+      { charSet: 'utf-8' },
+      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+    ],
+  }),
+  component: RootComponent,
+})
+
+function RootComponent() {
+  return (
+    <html>
+      <head>
+        <HydrationScript />
+        <HeadContent />
+      </head>
+      <body>
+        <nav
+          data-testid="main-nav"
+          style={{
+            padding: '12px 20px',
+            'border-bottom': '1px solid #e2e8f0',
+            display: 'flex',
+            'align-items': 'center',
+            gap: '16px',
+            'background-color': '#fff',
+          }}
+        >
+          <Link
+            to="/"
+            style={{ color: '#0284c7', 'text-decoration': 'none' }}
+            data-testid="nav-home"
+          >
+            Home (Global CSS)
+          </Link>
+          <Link
+            to="/modules"
+            style={{ color: '#0284c7', 'text-decoration': 'none' }}
+            data-testid="nav-modules"
+          >
+            CSS Modules
+          </Link>
+        </nav>
+
+        <main style={{ padding: '20px' }}>
+          <Outlet />
+        </main>
+        <Scripts />
+      </body>
+    </html>
+  )
+}
diff --git a/e2e/solid-start/css-modules/src/routes/index.tsx b/e2e/solid-start/css-modules/src/routes/index.tsx
new file mode 100644
index 0000000000..3c7caf6a97
--- /dev/null
+++ b/e2e/solid-start/css-modules/src/routes/index.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import '~/styles/global.css'
+
+export const Route = createFileRoute('/')({
+  component: Home,
+})
+
+function Home() {
+  return (
+    <div>
+      <h1>CSS Collection Test - Global CSS</h1>
+      <p>This page tests that global CSS is collected and served during SSR.</p>
+
+      <div class="global-container" data-testid="global-styled">
+        <div class="global-title" data-testid="global-title">
+          Global CSS Applied
+        </div>
+        <div class="global-description" data-testid="global-description">
+          This container should have a blue background, white text, and rounded
+          corners even with JavaScript disabled.
+        </div>
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/solid-start/css-modules/src/routes/modules.tsx b/e2e/solid-start/css-modules/src/routes/modules.tsx
new file mode 100644
index 0000000000..7bc20ae498
--- /dev/null
+++ b/e2e/solid-start/css-modules/src/routes/modules.tsx
@@ -0,0 +1,27 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import styles from '~/styles/card.module.css'
+
+export const Route = createFileRoute('/modules')({
+  component: Modules,
+})
+
+function Modules() {
+  return (
+    <div>
+      <h1>CSS Collection Test - CSS Modules</h1>
+      <p>
+        This page tests that CSS modules are collected and served during SSR.
+      </p>
+
+      <div class={styles.card} data-testid="module-card">
+        <div class={styles.title} data-testid="module-title">
+          CSS Module Applied
+        </div>
+        <div class={styles.content} data-testid="module-content">
+          This card should have a green theme with scoped class names even with
+          JavaScript disabled.
+        </div>
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/solid-start/css-modules/src/styles/card.module.css b/e2e/solid-start/css-modules/src/styles/card.module.css
new file mode 100644
index 0000000000..fcbcb419aa
--- /dev/null
+++ b/e2e/solid-start/css-modules/src/styles/card.module.css
@@ -0,0 +1,20 @@
+/* CSS Module for testing scoped styles in dev mode */
+
+.card {
+  background-color: #f0fdf4; /* green-50 */
+  padding: 16px;
+  border-radius: 8px;
+  border: 1px solid #22c55e; /* green-500 */
+}
+
+.title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #166534; /* green-800 */
+  margin-bottom: 8px;
+}
+
+.content {
+  font-size: 14px;
+  color: #15803d; /* green-700 */
+}
diff --git a/e2e/solid-start/css-modules/src/styles/global.css b/e2e/solid-start/css-modules/src/styles/global.css
new file mode 100644
index 0000000000..5bef746780
--- /dev/null
+++ b/e2e/solid-start/css-modules/src/styles/global.css
@@ -0,0 +1,19 @@
+/* Global styles for testing CSS collection in dev mode */
+
+.global-container {
+  background-color: #3b82f6; /* blue-500 */
+  padding: 24px;
+  border-radius: 12px;
+  color: white;
+}
+
+.global-title {
+  font-size: 24px;
+  font-weight: bold;
+  margin-bottom: 16px;
+}
+
+.global-description {
+  font-size: 16px;
+  opacity: 0.9;
+}
diff --git a/e2e/solid-start/css-modules/tests/css.spec.ts b/e2e/solid-start/css-modules/tests/css.spec.ts
new file mode 100644
index 0000000000..ec4955f9b2
--- /dev/null
+++ b/e2e/solid-start/css-modules/tests/css.spec.ts
@@ -0,0 +1,137 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+test.describe('CSS styles in SSR (dev mode)', () => {
+  test.describe('with JavaScript disabled', () => {
+    test.use({ javaScriptEnabled: false })
+
+    test('global CSS is applied on initial page load', async ({ page }) => {
+      await page.goto('/')
+
+      const element = page.getByTestId('global-styled')
+      await expect(element).toBeVisible()
+
+      // Verify the CSS is applied by checking computed styles
+      // #3b82f6 (blue-500) in RGB is rgb(59, 130, 246)
+      const backgroundColor = await element.evaluate(
+        (el) => getComputedStyle(el).backgroundColor,
+      )
+      expect(backgroundColor).toBe('rgb(59, 130, 246)')
+
+      const padding = await element.evaluate(
+        (el) => getComputedStyle(el).padding,
+      )
+      expect(padding).toBe('24px')
+
+      const borderRadius = await element.evaluate(
+        (el) => getComputedStyle(el).borderRadius,
+      )
+      expect(borderRadius).toBe('12px')
+    })
+
+    test('CSS modules are applied on initial page load', async ({ page }) => {
+      await page.goto('/modules')
+
+      const card = page.getByTestId('module-card')
+      await expect(card).toBeVisible()
+
+      // Verify class is scoped (hashed)
+      const className = await card.getAttribute('class')
+      expect(className).toBeTruthy()
+      expect(className).not.toBe('card')
+      // The class should contain some hash characters (CSS modules add a hash)
+      expect(className!.length).toBeGreaterThan(5)
+
+      // Verify computed styles from card.module.css
+      // #f0fdf4 (green-50) in RGB is rgb(240, 253, 244)
+      const backgroundColor = await card.evaluate(
+        (el) => getComputedStyle(el).backgroundColor,
+      )
+      expect(backgroundColor).toBe('rgb(240, 253, 244)')
+
+      const padding = await card.evaluate((el) => getComputedStyle(el).padding)
+      expect(padding).toBe('16px')
+
+      const borderRadius = await card.evaluate(
+        (el) => getComputedStyle(el).borderRadius,
+      )
+      expect(borderRadius).toBe('8px')
+    })
+
+    test('global CSS class names are NOT scoped', async ({ page }) => {
+      await page.goto('/')
+
+      const element = page.getByTestId('global-styled')
+      await expect(element).toBeVisible()
+
+      // Get the class attribute - it should be the plain class name (not hashed)
+      const className = await element.getAttribute('class')
+      expect(className).toBe('global-container')
+    })
+  })
+
+  test('styles persist after hydration', async ({ page }) => {
+    await page.goto('/')
+
+    // Wait for hydration
+    await page.waitForTimeout(1000)
+
+    const element = page.getByTestId('global-styled')
+    const backgroundColor = await element.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(59, 130, 246)')
+  })
+
+  test('CSS modules styles persist after hydration', async ({ page }) => {
+    await page.goto('/modules')
+
+    // Wait for hydration
+    await page.waitForTimeout(1000)
+
+    const card = page.getByTestId('module-card')
+    const backgroundColor = await card.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(240, 253, 244)')
+  })
+
+  test('styles work correctly after client-side navigation', async ({
+    page,
+  }) => {
+    // Start from home
+    await page.goto('/')
+    await page.waitForTimeout(1000)
+
+    // Verify initial styles
+    const globalElement = page.getByTestId('global-styled')
+    await expect(globalElement).toBeVisible()
+    let backgroundColor = await globalElement.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(59, 130, 246)')
+
+    // Navigate to modules page
+    await page.getByTestId('nav-modules').click()
+    await page.waitForURL('/modules')
+
+    // Verify CSS modules styles
+    const card = page.getByTestId('module-card')
+    await expect(card).toBeVisible()
+    backgroundColor = await card.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(240, 253, 244)')
+
+    // Navigate back to home
+    await page.getByTestId('nav-home').click()
+    await page.waitForURL('/')
+
+    // Verify global styles still work
+    await expect(globalElement).toBeVisible()
+    backgroundColor = await globalElement.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(59, 130, 246)')
+  })
+})
diff --git a/e2e/solid-start/css-modules/tests/setup/global.setup.ts b/e2e/solid-start/css-modules/tests/setup/global.setup.ts
new file mode 100644
index 0000000000..3593d10ab9
--- /dev/null
+++ b/e2e/solid-start/css-modules/tests/setup/global.setup.ts
@@ -0,0 +1,6 @@
+import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function setup() {
+  await e2eStartDummyServer(packageJson.name)
+}
diff --git a/e2e/solid-start/css-modules/tests/setup/global.teardown.ts b/e2e/solid-start/css-modules/tests/setup/global.teardown.ts
new file mode 100644
index 0000000000..62fd79911c
--- /dev/null
+++ b/e2e/solid-start/css-modules/tests/setup/global.teardown.ts
@@ -0,0 +1,6 @@
+import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function teardown() {
+  await e2eStopDummyServer(packageJson.name)
+}
diff --git a/e2e/solid-start/css-modules/tsconfig.json b/e2e/solid-start/css-modules/tsconfig.json
new file mode 100644
index 0000000000..ed8b73fa2d
--- /dev/null
+++ b/e2e/solid-start/css-modules/tsconfig.json
@@ -0,0 +1,24 @@
+{
+  "include": ["**/*.ts", "**/*.tsx"],
+  "compilerOptions": {
+    "strict": true,
+    "esModuleInterop": true,
+    "jsx": "preserve",
+    "jsxImportSource": "solid-js",
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "lib": ["DOM", "DOM.Iterable", "ES2022"],
+    "isolatedModules": true,
+    "resolveJsonModule": true,
+    "skipLibCheck": true,
+    "target": "ES2022",
+    "allowJs": true,
+    "forceConsistentCasingInFileNames": true,
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["./src/*"]
+    },
+    "noEmit": true,
+    "types": ["vite/client"]
+  }
+}
diff --git a/e2e/solid-start/css-modules/vite.config.ts b/e2e/solid-start/css-modules/vite.config.ts
new file mode 100644
index 0000000000..1a2219f443
--- /dev/null
+++ b/e2e/solid-start/css-modules/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
+import viteSolid from 'vite-plugin-solid'
+
+export default defineConfig({
+  server: {
+    port: 3000,
+  },
+  plugins: [
+    tsConfigPaths({
+      projects: ['./tsconfig.json'],
+    }),
+    tanstackStart(),
+    viteSolid({ ssr: true }),
+  ],
+})
diff --git a/e2e/vue-start/css-modules/.gitignore b/e2e/vue-start/css-modules/.gitignore
new file mode 100644
index 0000000000..950326609c
--- /dev/null
+++ b/e2e/vue-start/css-modules/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+dist
+.routeTree.gen.ts
+src/routeTree.gen.ts
+test-results
+playwright-report
+port*.txt
diff --git a/e2e/vue-start/css-modules/.prettierignore b/e2e/vue-start/css-modules/.prettierignore
new file mode 100644
index 0000000000..083bdb7c4c
--- /dev/null
+++ b/e2e/vue-start/css-modules/.prettierignore
@@ -0,0 +1 @@
+src/routeTree.gen.ts
diff --git a/e2e/vue-start/css-modules/package.json b/e2e/vue-start/css-modules/package.json
new file mode 100644
index 0000000000..b2e267f9b5
--- /dev/null
+++ b/e2e/vue-start/css-modules/package.json
@@ -0,0 +1,31 @@
+{
+  "name": "tanstack-vue-start-e2e-css-modules",
+  "private": true,
+  "sideEffects": false,
+  "type": "module",
+  "scripts": {
+    "dev": "vite dev --port 3000",
+    "dev:e2e": "vite dev --port $PORT",
+    "build": "vite build && tsc --noEmit",
+    "preview": "vite preview",
+    "start": "pnpx srvx --prod -s ../client dist/server/server.js",
+    "test:e2e:dev": "MODE=dev playwright test --project=chromium",
+    "test:e2e:prod": "playwright test --project=chromium",
+    "test:e2e": "rm -rf port*.txt; pnpm run test:e2e:dev"
+  },
+  "dependencies": {
+    "@tanstack/vue-router": "workspace:^",
+    "@tanstack/vue-start": "workspace:^",
+    "vue": "^3.5.16"
+  },
+  "devDependencies": {
+    "@playwright/test": "^1.50.1",
+    "@tanstack/router-e2e-utils": "workspace:^",
+    "@types/node": "^22.10.2",
+    "@vitejs/plugin-vue-jsx": "^4.1.2",
+    "srvx": "^0.10.0",
+    "typescript": "^5.7.2",
+    "vite": "^7.1.7",
+    "vite-tsconfig-paths": "^5.1.4"
+  }
+}
diff --git a/e2e/vue-start/css-modules/playwright.config.ts b/e2e/vue-start/css-modules/playwright.config.ts
new file mode 100644
index 0000000000..7582bbbb53
--- /dev/null
+++ b/e2e/vue-start/css-modules/playwright.config.ts
@@ -0,0 +1,40 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const isDev = process.env.MODE === 'dev'
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+export default defineConfig({
+  testDir: './tests',
+  workers: 1,
+  reporter: [['line']],
+
+  globalSetup: './tests/setup/global.setup.ts',
+  globalTeardown: './tests/setup/global.teardown.ts',
+
+  use: {
+    baseURL,
+  },
+
+  webServer: {
+    command: isDev ? `pnpm dev:e2e` : `pnpm build && PORT=${PORT} pnpm start`,
+    url: baseURL,
+    reuseExistingServer: !process.env.CI,
+    stdout: 'pipe',
+    env: {
+      VITE_NODE_ENV: 'test',
+      PORT: String(PORT),
+    },
+  },
+
+  projects: [
+    {
+      name: 'chromium',
+      use: {
+        ...devices['Desktop Chrome'],
+      },
+    },
+  ],
+})
diff --git a/e2e/vue-start/css-modules/src/router.tsx b/e2e/vue-start/css-modules/src/router.tsx
new file mode 100644
index 0000000000..7394b7042e
--- /dev/null
+++ b/e2e/vue-start/css-modules/src/router.tsx
@@ -0,0 +1,12 @@
+import { createRouter } from '@tanstack/vue-router'
+import { routeTree } from './routeTree.gen'
+
+export function getRouter() {
+  const router = createRouter({
+    routeTree,
+    scrollRestoration: true,
+    defaultPreload: false,
+  })
+
+  return router
+}
diff --git a/e2e/vue-start/css-modules/src/routes/__root.tsx b/e2e/vue-start/css-modules/src/routes/__root.tsx
new file mode 100644
index 0000000000..39c0110bee
--- /dev/null
+++ b/e2e/vue-start/css-modules/src/routes/__root.tsx
@@ -0,0 +1,62 @@
+import {
+  Body,
+  HeadContent,
+  Html,
+  Link,
+  Outlet,
+  Scripts,
+  createRootRoute,
+} from '@tanstack/vue-router'
+
+export const Route = createRootRoute({
+  head: () => ({
+    meta: [
+      { charSet: 'utf-8' },
+      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+    ],
+  }),
+  component: RootComponent,
+})
+
+function RootComponent() {
+  return (
+    <Html>
+      <head>
+        <HeadContent />
+      </head>
+      <Body>
+        <nav
+          data-testid="main-nav"
+          style={{
+            padding: '12px 20px',
+            borderBottom: '1px solid #e2e8f0',
+            display: 'flex',
+            alignItems: 'center',
+            gap: '16px',
+            backgroundColor: '#fff',
+          }}
+        >
+          <Link
+            to="/"
+            style={{ color: '#0284c7', textDecoration: 'none' }}
+            data-testid="nav-home"
+          >
+            Home (Global CSS)
+          </Link>
+          <Link
+            to="/modules"
+            style={{ color: '#0284c7', textDecoration: 'none' }}
+            data-testid="nav-modules"
+          >
+            CSS Modules
+          </Link>
+        </nav>
+
+        <main style={{ padding: '20px' }}>
+          <Outlet />
+        </main>
+        <Scripts />
+      </Body>
+    </Html>
+  )
+}
diff --git a/e2e/vue-start/css-modules/src/routes/index.tsx b/e2e/vue-start/css-modules/src/routes/index.tsx
new file mode 100644
index 0000000000..5c52a6e342
--- /dev/null
+++ b/e2e/vue-start/css-modules/src/routes/index.tsx
@@ -0,0 +1,25 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import '~/styles/global.css'
+
+export const Route = createFileRoute('/')({
+  component: Home,
+})
+
+function Home() {
+  return (
+    <div>
+      <h1>CSS Collection Test - Global CSS</h1>
+      <p>This page tests that global CSS is collected and served during SSR.</p>
+
+      <div class="global-container" data-testid="global-styled">
+        <div class="global-title" data-testid="global-title">
+          Global CSS Applied
+        </div>
+        <div class="global-description" data-testid="global-description">
+          This container should have a blue background, white text, and rounded
+          corners even with JavaScript disabled.
+        </div>
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/vue-start/css-modules/src/routes/modules.tsx b/e2e/vue-start/css-modules/src/routes/modules.tsx
new file mode 100644
index 0000000000..7cc07119e4
--- /dev/null
+++ b/e2e/vue-start/css-modules/src/routes/modules.tsx
@@ -0,0 +1,27 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import styles from '~/styles/card.module.css'
+
+export const Route = createFileRoute('/modules')({
+  component: Modules,
+})
+
+function Modules() {
+  return (
+    <div>
+      <h1>CSS Collection Test - CSS Modules</h1>
+      <p>
+        This page tests that CSS modules are collected and served during SSR.
+      </p>
+
+      <div class={styles.card} data-testid="module-card">
+        <div class={styles.title} data-testid="module-title">
+          CSS Module Applied
+        </div>
+        <div class={styles.content} data-testid="module-content">
+          This card should have a green theme with scoped class names even with
+          JavaScript disabled.
+        </div>
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/vue-start/css-modules/src/styles/card.module.css b/e2e/vue-start/css-modules/src/styles/card.module.css
new file mode 100644
index 0000000000..fcbcb419aa
--- /dev/null
+++ b/e2e/vue-start/css-modules/src/styles/card.module.css
@@ -0,0 +1,20 @@
+/* CSS Module for testing scoped styles in dev mode */
+
+.card {
+  background-color: #f0fdf4; /* green-50 */
+  padding: 16px;
+  border-radius: 8px;
+  border: 1px solid #22c55e; /* green-500 */
+}
+
+.title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #166534; /* green-800 */
+  margin-bottom: 8px;
+}
+
+.content {
+  font-size: 14px;
+  color: #15803d; /* green-700 */
+}
diff --git a/e2e/vue-start/css-modules/src/styles/global.css b/e2e/vue-start/css-modules/src/styles/global.css
new file mode 100644
index 0000000000..5bef746780
--- /dev/null
+++ b/e2e/vue-start/css-modules/src/styles/global.css
@@ -0,0 +1,19 @@
+/* Global styles for testing CSS collection in dev mode */
+
+.global-container {
+  background-color: #3b82f6; /* blue-500 */
+  padding: 24px;
+  border-radius: 12px;
+  color: white;
+}
+
+.global-title {
+  font-size: 24px;
+  font-weight: bold;
+  margin-bottom: 16px;
+}
+
+.global-description {
+  font-size: 16px;
+  opacity: 0.9;
+}
diff --git a/e2e/vue-start/css-modules/tests/css.spec.ts b/e2e/vue-start/css-modules/tests/css.spec.ts
new file mode 100644
index 0000000000..ec4955f9b2
--- /dev/null
+++ b/e2e/vue-start/css-modules/tests/css.spec.ts
@@ -0,0 +1,137 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+test.describe('CSS styles in SSR (dev mode)', () => {
+  test.describe('with JavaScript disabled', () => {
+    test.use({ javaScriptEnabled: false })
+
+    test('global CSS is applied on initial page load', async ({ page }) => {
+      await page.goto('/')
+
+      const element = page.getByTestId('global-styled')
+      await expect(element).toBeVisible()
+
+      // Verify the CSS is applied by checking computed styles
+      // #3b82f6 (blue-500) in RGB is rgb(59, 130, 246)
+      const backgroundColor = await element.evaluate(
+        (el) => getComputedStyle(el).backgroundColor,
+      )
+      expect(backgroundColor).toBe('rgb(59, 130, 246)')
+
+      const padding = await element.evaluate(
+        (el) => getComputedStyle(el).padding,
+      )
+      expect(padding).toBe('24px')
+
+      const borderRadius = await element.evaluate(
+        (el) => getComputedStyle(el).borderRadius,
+      )
+      expect(borderRadius).toBe('12px')
+    })
+
+    test('CSS modules are applied on initial page load', async ({ page }) => {
+      await page.goto('/modules')
+
+      const card = page.getByTestId('module-card')
+      await expect(card).toBeVisible()
+
+      // Verify class is scoped (hashed)
+      const className = await card.getAttribute('class')
+      expect(className).toBeTruthy()
+      expect(className).not.toBe('card')
+      // The class should contain some hash characters (CSS modules add a hash)
+      expect(className!.length).toBeGreaterThan(5)
+
+      // Verify computed styles from card.module.css
+      // #f0fdf4 (green-50) in RGB is rgb(240, 253, 244)
+      const backgroundColor = await card.evaluate(
+        (el) => getComputedStyle(el).backgroundColor,
+      )
+      expect(backgroundColor).toBe('rgb(240, 253, 244)')
+
+      const padding = await card.evaluate((el) => getComputedStyle(el).padding)
+      expect(padding).toBe('16px')
+
+      const borderRadius = await card.evaluate(
+        (el) => getComputedStyle(el).borderRadius,
+      )
+      expect(borderRadius).toBe('8px')
+    })
+
+    test('global CSS class names are NOT scoped', async ({ page }) => {
+      await page.goto('/')
+
+      const element = page.getByTestId('global-styled')
+      await expect(element).toBeVisible()
+
+      // Get the class attribute - it should be the plain class name (not hashed)
+      const className = await element.getAttribute('class')
+      expect(className).toBe('global-container')
+    })
+  })
+
+  test('styles persist after hydration', async ({ page }) => {
+    await page.goto('/')
+
+    // Wait for hydration
+    await page.waitForTimeout(1000)
+
+    const element = page.getByTestId('global-styled')
+    const backgroundColor = await element.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(59, 130, 246)')
+  })
+
+  test('CSS modules styles persist after hydration', async ({ page }) => {
+    await page.goto('/modules')
+
+    // Wait for hydration
+    await page.waitForTimeout(1000)
+
+    const card = page.getByTestId('module-card')
+    const backgroundColor = await card.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(240, 253, 244)')
+  })
+
+  test('styles work correctly after client-side navigation', async ({
+    page,
+  }) => {
+    // Start from home
+    await page.goto('/')
+    await page.waitForTimeout(1000)
+
+    // Verify initial styles
+    const globalElement = page.getByTestId('global-styled')
+    await expect(globalElement).toBeVisible()
+    let backgroundColor = await globalElement.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(59, 130, 246)')
+
+    // Navigate to modules page
+    await page.getByTestId('nav-modules').click()
+    await page.waitForURL('/modules')
+
+    // Verify CSS modules styles
+    const card = page.getByTestId('module-card')
+    await expect(card).toBeVisible()
+    backgroundColor = await card.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(240, 253, 244)')
+
+    // Navigate back to home
+    await page.getByTestId('nav-home').click()
+    await page.waitForURL('/')
+
+    // Verify global styles still work
+    await expect(globalElement).toBeVisible()
+    backgroundColor = await globalElement.evaluate(
+      (el) => getComputedStyle(el).backgroundColor,
+    )
+    expect(backgroundColor).toBe('rgb(59, 130, 246)')
+  })
+})
diff --git a/e2e/vue-start/css-modules/tests/setup/global.setup.ts b/e2e/vue-start/css-modules/tests/setup/global.setup.ts
new file mode 100644
index 0000000000..3593d10ab9
--- /dev/null
+++ b/e2e/vue-start/css-modules/tests/setup/global.setup.ts
@@ -0,0 +1,6 @@
+import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function setup() {
+  await e2eStartDummyServer(packageJson.name)
+}
diff --git a/e2e/vue-start/css-modules/tests/setup/global.teardown.ts b/e2e/vue-start/css-modules/tests/setup/global.teardown.ts
new file mode 100644
index 0000000000..62fd79911c
--- /dev/null
+++ b/e2e/vue-start/css-modules/tests/setup/global.teardown.ts
@@ -0,0 +1,6 @@
+import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function teardown() {
+  await e2eStopDummyServer(packageJson.name)
+}
diff --git a/e2e/vue-start/css-modules/tsconfig.json b/e2e/vue-start/css-modules/tsconfig.json
new file mode 100644
index 0000000000..8bf3d33789
--- /dev/null
+++ b/e2e/vue-start/css-modules/tsconfig.json
@@ -0,0 +1,24 @@
+{
+  "include": ["**/*.ts", "**/*.tsx"],
+  "compilerOptions": {
+    "strict": true,
+    "esModuleInterop": true,
+    "jsx": "preserve",
+    "jsxImportSource": "vue",
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "lib": ["DOM", "DOM.Iterable", "ES2022"],
+    "isolatedModules": true,
+    "resolveJsonModule": true,
+    "skipLibCheck": true,
+    "target": "ES2022",
+    "allowJs": true,
+    "forceConsistentCasingInFileNames": true,
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["./src/*"]
+    },
+    "noEmit": true,
+    "types": ["vite/client"]
+  }
+}
diff --git a/e2e/vue-start/css-modules/vite.config.ts b/e2e/vue-start/css-modules/vite.config.ts
new file mode 100644
index 0000000000..d7b96c1c2f
--- /dev/null
+++ b/e2e/vue-start/css-modules/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import tsConfigPaths from 'vite-tsconfig-paths'
+import { tanstackStart } from '@tanstack/vue-start/plugin/vite'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
+export default defineConfig({
+  server: {
+    port: 3000,
+  },
+  plugins: [
+    tsConfigPaths({
+      projects: ['./tsconfig.json'],
+    }),
+    tanstackStart(),
+    vueJsx(),
+  ],
+})
diff --git a/packages/react-router/src/HeadContent.tsx b/packages/react-router/src/HeadContent.tsx
index 3a30c521dc..8da3b5b279 100644
--- a/packages/react-router/src/HeadContent.tsx
+++ b/packages/react-router/src/HeadContent.tsx
@@ -203,6 +203,39 @@ export const useTags = () => {
   )
 }
 
+/**
+ * Renders a stylesheet link for dev mode CSS collection.
+ * On the server, renders the full link with route-scoped CSS URL.
+ * On the client, renders the same link to avoid hydration mismatch,
+ * then removes it after hydration since Vite's HMR handles CSS updates.
+ */
+function DevStylesLink() {
+  const router = useRouter()
+
+  const routeIds = useRouterState({
+    select: (state) => state.matches.map((match) => match.routeId),
+  })
+
+  React.useEffect(() => {
+    // After hydration, remove the SSR-rendered dev styles link
+    document
+      .querySelectorAll('[data-tanstack-start-dev-styles]')
+      .forEach((el) => el.remove())
+  }, [])
+
+  // Build the same href on both server and client for hydration match
+  const href = `/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds.join(','))}`
+
+  return (
+    <link
+      rel="stylesheet"
+      href={href}
+      data-tanstack-start-dev-styles
+      suppressHydrationWarning
+    />
+  )
+}
+
 /**
  * Render route-managed head tags (title, meta, links, styles, head scripts).
  * Place inside the document head of your app shell.
@@ -212,9 +245,14 @@ export function HeadContent() {
   const tags = useTags()
   const router = useRouter()
   const nonce = router.options.ssr?.nonce
-  return tags.map((tag) => (
-    <Asset {...tag} key={`tsr-meta-${JSON.stringify(tag)}`} nonce={nonce} />
-  ))
+  return (
+    <>
+      {process.env.NODE_ENV !== 'production' && <DevStylesLink />}
+      {tags.map((tag) => (
+        <Asset {...tag} key={`tsr-meta-${JSON.stringify(tag)}`} nonce={nonce} />
+      ))}
+    </>
+  )
 }
 
 function uniqBy<T>(arr: Array<T>, fn: (item: T) => string) {
diff --git a/packages/solid-router/src/HeadContent.tsx b/packages/solid-router/src/HeadContent.tsx
index 3232697676..2d52ac88a7 100644
--- a/packages/solid-router/src/HeadContent.tsx
+++ b/packages/solid-router/src/HeadContent.tsx
@@ -1,6 +1,6 @@
 import * as Solid from 'solid-js'
 import { MetaProvider } from '@solidjs/meta'
-import { For } from 'solid-js'
+import { For, onMount } from 'solid-js'
 import { escapeHtml } from '@tanstack/router-core'
 import { Asset } from './Asset'
 import { useRouter } from './useRouter'
@@ -197,6 +197,31 @@ export const useTags = () => {
     )
 }
 
+/**
+ * Renders a stylesheet link for dev mode CSS collection.
+ * On the server, renders the full link with route-scoped CSS URL.
+ * On the client, renders the same link to avoid hydration mismatch,
+ * then removes it after hydration since Vite's HMR handles CSS updates.
+ */
+function DevStylesLink() {
+  const routeIds = useRouterState({
+    select: (state) => state.matches.map((match) => match.routeId),
+  })
+
+  onMount(() => {
+    // After hydration, remove the SSR-rendered dev styles link
+    document
+      .querySelectorAll('[data-tanstack-start-dev-styles]')
+      .forEach((el) => el.remove())
+  })
+
+  // Build the same href on both server and client for hydration match
+  const href = () =>
+    `/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds().join(','))}`
+
+  return <link rel="stylesheet" href={href()} data-tanstack-start-dev-styles />
+}
+
 /**
  * @description The `HeadContent` component is used to render meta tags, links, and scripts for the current route.
  * When using full document hydration (hydrating from `<html>`), this component should be rendered in the `<body>`
@@ -208,6 +233,7 @@ export function HeadContent() {
 
   return (
     <MetaProvider>
+      {process.env.NODE_ENV !== 'production' && <DevStylesLink />}
       <For each={tags()}>{(tag) => <Asset {...tag} />}</For>
     </MetaProvider>
   )
diff --git a/packages/start-plugin-core/src/dev-server-plugin/dev-styles.ts b/packages/start-plugin-core/src/dev-server-plugin/dev-styles.ts
new file mode 100644
index 0000000000..4c226ae425
--- /dev/null
+++ b/packages/start-plugin-core/src/dev-server-plugin/dev-styles.ts
@@ -0,0 +1,176 @@
+/**
+ * CSS collection for dev mode.
+ * Crawls the Vite module graph to collect CSS from the router entry and all its dependencies.
+ */
+import type { ModuleNode, ViteDevServer } from 'vite'
+
+// CSS file extensions supported by Vite
+const CSS_FILE_REGEX =
+  /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/
+// URL params that indicate CSS should not be injected (e.g., ?url, ?inline)
+const CSS_SIDE_EFFECT_FREE_PARAMS = ['url', 'inline', 'raw', 'inline-css']
+
+function isCssFile(file: string): boolean {
+  return CSS_FILE_REGEX.test(file)
+}
+
+function hasCssSideEffectFreeParam(url: string): boolean {
+  const queryString = url.split('?')[1]
+  if (!queryString) return false
+
+  const params = new URLSearchParams(queryString)
+  return CSS_SIDE_EFFECT_FREE_PARAMS.some(
+    (param) =>
+      params.get(param) === '' &&
+      !url.includes(`?${param}=`) &&
+      !url.includes(`&${param}=`),
+  )
+}
+
+export interface CollectDevStylesOptions {
+  viteDevServer: ViteDevServer
+  entries: Array<string>
+}
+
+/**
+ * Collect CSS content from the module graph starting from the given entry points.
+ */
+export async function collectDevStyles(
+  opts: CollectDevStylesOptions,
+): Promise<string | undefined> {
+  const { viteDevServer, entries } = opts
+  const styles: Map<string, string> = new Map()
+  const visited = new Set<ModuleNode>()
+
+  for (const entry of entries) {
+    const normalizedPath = entry.replace(/\\/g, '/')
+    let node = await viteDevServer.moduleGraph.getModuleById(normalizedPath)
+
+    // If module isn't in the graph yet, request it to trigger transform
+    if (!node) {
+      try {
+        await viteDevServer.transformRequest(normalizedPath)
+      } catch (err) {
+        // Ignore - the module might not exist yet
+      }
+      node = await viteDevServer.moduleGraph.getModuleById(normalizedPath)
+    }
+
+    if (node) {
+      await crawlModuleForCss(viteDevServer, node, visited, styles)
+    }
+  }
+
+  if (styles.size === 0) return undefined
+
+  return Array.from(styles.entries())
+    .map(([fileName, css]) => {
+      const escapedFileName = fileName
+        .replace(/\/\*/g, '/\\*')
+        .replace(/\*\//g, '*\\/')
+      return `\n/* ${escapedFileName} */\n${css}`
+    })
+    .join('\n')
+}
+
+async function crawlModuleForCss(
+  vite: ViteDevServer,
+  node: ModuleNode,
+  visited: Set<ModuleNode>,
+  styles: Map<string, string>,
+): Promise<void> {
+  if (visited.has(node)) return
+  visited.add(node)
+
+  const branches: Array<Promise<void>> = []
+
+  // Ensure the module has been transformed to populate its deps
+  // This is important for code-split modules that may not have been processed yet
+  if (!node.ssrTransformResult) {
+    try {
+      await vite.transformRequest(node.url, { ssr: true })
+      // Re-fetch the node to get updated state
+      const updatedNode = await vite.moduleGraph.getModuleByUrl(node.url)
+      if (updatedNode) {
+        node = updatedNode
+      }
+    } catch {
+      // Ignore transform errors - the module might not be transformable
+    }
+  }
+
+  // Check if this is a CSS file
+  if (
+    node.file &&
+    isCssFile(node.file) &&
+    !hasCssSideEffectFreeParam(node.url)
+  ) {
+    const css = await loadCssContent(vite, node)
+    if (css) {
+      styles.set(node.url, css)
+    }
+  }
+
+  // Crawl dependencies using ssrTransformResult.deps and importedModules
+  // We need both because:
+  // 1. ssrTransformResult.deps has resolved URLs for SSR dependencies
+  // 2. importedModules may contain CSS files and code-split modules not in SSR deps
+  const depsFromSsr = node.ssrTransformResult?.deps ?? []
+  const urlsToVisit = new Set<string>(depsFromSsr)
+
+  // Check importedModules for CSS files and additional modules
+  for (const importedNode of node.importedModules) {
+    if (importedNode.file && isCssFile(importedNode.file)) {
+      // CSS files often don't appear in ssrTransformResult.deps, add them explicitly
+      branches.push(crawlModuleForCss(vite, importedNode, visited, styles))
+    } else if (!urlsToVisit.has(importedNode.url)) {
+      // Also add non-CSS imports that aren't in SSR deps (e.g., code-split modules)
+      urlsToVisit.add(importedNode.url)
+    }
+  }
+
+  for (const depUrl of urlsToVisit) {
+    branches.push(
+      (async () => {
+        const depNode = await vite.moduleGraph.getModuleByUrl(depUrl)
+        if (depNode) {
+          await crawlModuleForCss(vite, depNode, visited, styles)
+        }
+      })(),
+    )
+  }
+
+  await Promise.all(branches)
+}
+
+async function loadCssContent(
+  vite: ViteDevServer,
+  node: ModuleNode,
+): Promise<string | undefined> {
+  // For ALL CSS files (including CSS modules), get the transformed content
+  // and extract __vite__css. Vite's transform puts the final CSS (with hashed
+  // class names for modules) into the __vite__css variable.
+  const transformResult = await vite.transformRequest(node.url)
+  if (!transformResult?.code) return undefined
+
+  // Extract CSS content from Vite's transformed module
+  return extractCssFromViteModule(transformResult.code)
+}
+
+/**
+ * Extract CSS string from Vite's transformed CSS module code.
+ * Vite wraps CSS content in a JS module with __vite__css variable.
+ */
+function extractCssFromViteModule(code: string): string | undefined {
+  // Match: const __vite__css = "..."
+  const match = code.match(/const\s+__vite__css\s*=\s*["'`]([\s\S]*?)["'`]/)
+  if (match?.[1]) {
+    // Unescape the string
+    return match[1]
+      .replace(/\\n/g, '\n')
+      .replace(/\\t/g, '\t')
+      .replace(/\\"/g, '"')
+      .replace(/\\\\/g, '\\')
+  }
+  return undefined
+}
diff --git a/packages/start-plugin-core/src/dev-server-plugin/plugin.ts b/packages/start-plugin-core/src/dev-server-plugin/plugin.ts
index fccbfb09de..1fe8856310 100644
--- a/packages/start-plugin-core/src/dev-server-plugin/plugin.ts
+++ b/packages/start-plugin-core/src/dev-server-plugin/plugin.ts
@@ -4,13 +4,14 @@ import { NodeRequest, sendNodeResponse } from 'srvx/node'
 import { ENTRY_POINTS, VITE_ENVIRONMENT_NAMES } from '../constants'
 import { resolveViteId } from '../utils'
 import { extractHtmlScripts } from './extract-html-scripts'
+import { collectDevStyles } from './dev-styles'
 import type { Connect, DevEnvironment, PluginOption } from 'vite'
-import type { TanStackStartOutputConfig } from '../schema'
+import type { GetConfigFn } from '../types'
 
 export function devServerPlugin({
   getConfig,
 }: {
-  getConfig: () => { startConfig: TanStackStartOutputConfig }
+  getConfig: GetConfigFn
 }): PluginOption {
   let isTest = false
 
@@ -75,6 +76,52 @@ export function devServerPlugin({
             )
           }
 
+          // Middleware to serve collected CSS for dev mode
+          // Security: Route IDs from query params are validated against TSS_ROUTES_MANIFEST.
+          // Only routes that exist in the manifest will have their CSS collected.
+          // Arbitrary file paths cannot be injected.
+          viteDevServer.middlewares.use(async (req, res, next) => {
+            const url = req.url ?? ''
+            if (!url.startsWith('/@tanstack-start/styles.css')) {
+              return next()
+            }
+
+            // Parse route IDs from query param
+            const urlObj = new URL(url, 'http://localhost')
+            const routesParam = urlObj.searchParams.get('routes')
+            const routeIds = routesParam ? routesParam.split(',') : []
+
+            // Build entries list from route file paths
+            const entries: Array<string> = []
+
+            // Look up route file paths from manifest
+            // Only routes registered in the manifest are used - this prevents path injection
+            const routesManifest = (globalThis as any).TSS_ROUTES_MANIFEST as
+              | Record<string, { filePath: string; children?: Array<string> }>
+              | undefined
+
+            if (routesManifest && routeIds.length > 0) {
+              for (const routeId of routeIds) {
+                const route = routesManifest[routeId]
+                if (route?.filePath) {
+                  entries.push(route.filePath)
+                }
+              }
+            }
+
+            const css =
+              entries.length > 0
+                ? await collectDevStyles({
+                    viteDevServer,
+                    entries,
+                  })
+                : undefined
+
+            res.setHeader('Content-Type', 'text/css')
+            res.setHeader('Cache-Control', 'no-store')
+            res.end(css ?? '')
+          })
+
           viteDevServer.middlewares.use(async (req, res) => {
             // fix the request URL to match the original URL
             // otherwise, the request URL will '/index.html'
diff --git a/packages/vue-router/src/HeadContent.tsx b/packages/vue-router/src/HeadContent.tsx
index 1baeef6a1f..a7666d20a7 100644
--- a/packages/vue-router/src/HeadContent.tsx
+++ b/packages/vue-router/src/HeadContent.tsx
@@ -6,6 +6,41 @@ import { useRouter } from './useRouter'
 import { useRouterState } from './useRouterState'
 import type { RouterManagedTag } from '@tanstack/router-core'
 
+/**
+ * Renders a stylesheet link for dev mode CSS collection.
+ * On the server, renders the full link with route-scoped CSS URL.
+ * On the client, renders the same link to avoid hydration mismatch,
+ * then removes it after hydration since Vite's HMR handles CSS updates.
+ */
+const DevStylesLink = Vue.defineComponent({
+  name: 'DevStylesLink',
+  setup() {
+    const routeIds = useRouterState({
+      select: (state) => state.matches.map((match) => match.routeId),
+    })
+
+    Vue.onMounted(() => {
+      // After hydration, remove the SSR-rendered dev styles link
+      document
+        .querySelectorAll('[data-tanstack-start-dev-styles]')
+        .forEach((el) => el.remove())
+    })
+
+    // Build the same href on both server and client for hydration match
+    const href = Vue.computed(
+      () =>
+        `/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds.value.join(','))}`,
+    )
+
+    return () =>
+      Vue.h('link', {
+        rel: 'stylesheet',
+        href: href.value,
+        'data-tanstack-start-dev-styles': true,
+      })
+  },
+})
+
 export const useTags = () => {
   const router = useRouter()
 
@@ -152,12 +187,19 @@ export const HeadContent = Vue.defineComponent({
     const tags = useTags()
 
     return () => {
-      return tags().map((tag) =>
+      const children = tags().map((tag) =>
         Vue.h(Asset, {
           ...tag,
           key: `tsr-meta-${JSON.stringify(tag)}`,
         }),
       )
+
+      // In dev mode, prepend the DevStylesLink
+      if (process.env.NODE_ENV !== 'production') {
+        return [Vue.h(DevStylesLink), ...children]
+      }
+
+      return children
     }
   },
 })
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 18462b46d8..57b57bc2d8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1516,6 +1516,52 @@ importers:
         specifier: ^5.1.4
         version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
 
+  e2e/react-start/css-modules:
+    dependencies:
+      '@tanstack/react-router':
+        specifier: workspace:*
+        version: link:../../../packages/react-router
+      '@tanstack/react-start':
+        specifier: workspace:*
+        version: link:../../../packages/react-start
+      react:
+        specifier: ^19.2.0
+        version: 19.2.0
+      react-dom:
+        specifier: ^19.2.0
+        version: 19.2.0(react@19.2.0)
+    devDependencies:
+      '@playwright/test':
+        specifier: ^1.56.1
+        version: 1.56.1
+      '@tanstack/router-e2e-utils':
+        specifier: workspace:^
+        version: link:../../e2e-utils
+      '@types/node':
+        specifier: 22.10.2
+        version: 22.10.2
+      '@types/react':
+        specifier: ^19.2.2
+        version: 19.2.2
+      '@types/react-dom':
+        specifier: ^19.2.2
+        version: 19.2.2(@types/react@19.2.2)
+      '@vitejs/plugin-react':
+        specifier: ^4.3.4
+        version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      srvx:
+        specifier: ^0.10.0
+        version: 0.10.0
+      typescript:
+        specifier: ^5.7.2
+        version: 5.9.2
+      vite:
+        specifier: ^7.1.7
+        version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+      vite-tsconfig-paths:
+        specifier: ^5.1.4
+        version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
   e2e/react-start/custom-basepath:
     dependencies:
       '@tanstack/react-router':
@@ -3352,6 +3398,43 @@ importers:
         specifier: ^5.1.4
         version: 5.1.4(typescript@5.8.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
 
+  e2e/solid-start/css-modules:
+    dependencies:
+      '@tanstack/solid-router':
+        specifier: workspace:^
+        version: link:../../../packages/solid-router
+      '@tanstack/solid-start':
+        specifier: workspace:*
+        version: link:../../../packages/solid-start
+      solid-js:
+        specifier: 1.9.10
+        version: 1.9.10
+    devDependencies:
+      '@playwright/test':
+        specifier: ^1.56.1
+        version: 1.56.1
+      '@tanstack/router-e2e-utils':
+        specifier: workspace:^
+        version: link:../../e2e-utils
+      '@types/node':
+        specifier: 22.10.2
+        version: 22.10.2
+      srvx:
+        specifier: ^0.10.0
+        version: 0.10.0
+      typescript:
+        specifier: ^5.7.2
+        version: 5.9.2
+      vite:
+        specifier: ^7.1.7
+        version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+      vite-plugin-solid:
+        specifier: ^2.11.10
+        version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      vite-tsconfig-paths:
+        specifier: ^5.1.4
+        version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
   e2e/solid-start/custom-basepath:
     dependencies:
       '@tanstack/solid-router':
@@ -5103,6 +5186,43 @@ importers:
         specifier: ^5.1.4
         version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
 
+  e2e/vue-start/css-modules:
+    dependencies:
+      '@tanstack/vue-router':
+        specifier: workspace:*
+        version: link:../../../packages/vue-router
+      '@tanstack/vue-start':
+        specifier: workspace:*
+        version: link:../../../packages/vue-start
+      vue:
+        specifier: ^3.5.16
+        version: 3.5.25(typescript@5.9.2)
+    devDependencies:
+      '@playwright/test':
+        specifier: ^1.56.1
+        version: 1.56.1
+      '@tanstack/router-e2e-utils':
+        specifier: workspace:^
+        version: link:../../e2e-utils
+      '@types/node':
+        specifier: 22.10.2
+        version: 22.10.2
+      '@vitejs/plugin-vue-jsx':
+        specifier: ^4.1.2
+        version: 4.2.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2))
+      srvx:
+        specifier: ^0.10.0
+        version: 0.10.0
+      typescript:
+        specifier: ^5.7.2
+        version: 5.9.2
+      vite:
+        specifier: ^7.1.7
+        version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+      vite-tsconfig-paths:
+        specifier: ^5.1.4
+        version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
   e2e/vue-start/custom-basepath:
     dependencies:
       '@tanstack/vue-router':
@@ -10380,7 +10500,7 @@ importers:
     devDependencies:
       '@netlify/vite-plugin-tanstack-start':
         specifier: ^1.1.4
-        version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+        version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
       '@tailwindcss/vite':
         specifier: ^4.1.18
         version: 4.1.18(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
@@ -26708,13 +26828,13 @@ snapshots:
       uuid: 11.1.0
       write-file-atomic: 5.0.1
 
-  '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)':
+  '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)':
     dependencies:
       '@netlify/blobs': 10.1.0
       '@netlify/config': 23.2.0
       '@netlify/dev-utils': 4.3.0
       '@netlify/edge-functions-dev': 1.0.0
-      '@netlify/functions-dev': 1.0.0(rollup@4.52.5)
+      '@netlify/functions-dev': 1.0.0(encoding@0.1.13)(rollup@4.52.5)
       '@netlify/headers': 2.1.0
       '@netlify/images': 1.3.0(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)
       '@netlify/redirects': 3.1.0
@@ -26782,12 +26902,12 @@ snapshots:
     dependencies:
       '@netlify/types': 2.1.0
 
-  '@netlify/functions-dev@1.0.0(rollup@4.52.5)':
+  '@netlify/functions-dev@1.0.0(encoding@0.1.13)(rollup@4.52.5)':
     dependencies:
       '@netlify/blobs': 10.1.0
       '@netlify/dev-utils': 4.3.0
       '@netlify/functions': 5.0.0
-      '@netlify/zip-it-and-ship-it': 14.1.11(rollup@4.52.5)
+      '@netlify/zip-it-and-ship-it': 14.1.11(encoding@0.1.13)(rollup@4.52.5)
       cron-parser: 4.9.0
       decache: 4.6.2
       extract-zip: 2.0.1
@@ -26877,9 +26997,9 @@ snapshots:
 
   '@netlify/types@2.1.0': {}
 
-  '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
+  '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
     dependencies:
-      '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+      '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
       vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
     optionalDependencies:
       '@tanstack/solid-start': link:packages/solid-start
@@ -26907,9 +27027,9 @@ snapshots:
       - supports-color
       - uploadthing
 
-  '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
+  '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
     dependencies:
-      '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)
+      '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)
       '@netlify/dev-utils': 4.3.0
       dedent: 1.7.0(babel-plugin-macros@3.1.0)
       vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
@@ -26937,13 +27057,13 @@ snapshots:
       - supports-color
       - uploadthing
 
-  '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.52.5)':
+  '@netlify/zip-it-and-ship-it@14.1.11(encoding@0.1.13)(rollup@4.52.5)':
     dependencies:
       '@babel/parser': 7.28.5
       '@babel/types': 7.28.4
       '@netlify/binary-info': 1.0.0
       '@netlify/serverless-functions-api': 2.7.1
-      '@vercel/nft': 0.29.4(rollup@4.52.5)
+      '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.52.5)
       archiver: 7.0.1
       common-path-prefix: 3.0.0
       copy-file: 11.1.0
@@ -30198,7 +30318,7 @@ snapshots:
   '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
     optional: true
 
-  '@vercel/nft@0.29.4(rollup@4.52.5)':
+  '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.52.5)':
     dependencies:
       '@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13)
       '@rollup/pluginutils': 5.1.4(rollup@4.52.5)

From 453af57d921f11f2431ce999390af236b3f1c8cf Mon Sep 17 00:00:00 2001
From: Manuel Schiller <manuel.schiller@caligano.de>
Date: Sat, 10 Jan 2026 03:52:45 +0100
Subject: [PATCH 2/3] fix: add Vite client reference to module routes for CSS
 modules

---
 e2e/react-start/css-modules/src/routes/modules.tsx | 1 +
 e2e/solid-start/css-modules/src/routes/modules.tsx | 1 +
 e2e/vue-start/css-modules/src/routes/modules.tsx   | 1 +
 3 files changed, 3 insertions(+)

diff --git a/e2e/react-start/css-modules/src/routes/modules.tsx b/e2e/react-start/css-modules/src/routes/modules.tsx
index 101a612b8b..3b812a5d4e 100644
--- a/e2e/react-start/css-modules/src/routes/modules.tsx
+++ b/e2e/react-start/css-modules/src/routes/modules.tsx
@@ -1,3 +1,4 @@
+/// <reference types="vite/client" />
 import { createFileRoute } from '@tanstack/react-router'
 import styles from '~/styles/card.module.css'
 
diff --git a/e2e/solid-start/css-modules/src/routes/modules.tsx b/e2e/solid-start/css-modules/src/routes/modules.tsx
index 7bc20ae498..34cd8d36ea 100644
--- a/e2e/solid-start/css-modules/src/routes/modules.tsx
+++ b/e2e/solid-start/css-modules/src/routes/modules.tsx
@@ -1,3 +1,4 @@
+/// <reference types="vite/client" />
 import { createFileRoute } from '@tanstack/solid-router'
 import styles from '~/styles/card.module.css'
 
diff --git a/e2e/vue-start/css-modules/src/routes/modules.tsx b/e2e/vue-start/css-modules/src/routes/modules.tsx
index 7cc07119e4..4c1a655ed8 100644
--- a/e2e/vue-start/css-modules/src/routes/modules.tsx
+++ b/e2e/vue-start/css-modules/src/routes/modules.tsx
@@ -1,3 +1,4 @@
+/// <reference types="vite/client" />
 import { createFileRoute } from '@tanstack/vue-router'
 import styles from '~/styles/card.module.css'
 

From ae8fd5946cd8260cb2888f33cd28fb3f91fa10c3 Mon Sep 17 00:00:00 2001
From: Manuel Schiller <manuel.schiller@caligano.de>
Date: Sat, 10 Jan 2026 03:55:21 +0100
Subject: [PATCH 3/3] fix test expectation

---
 packages/react-router/tests/Scripts.test.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/react-router/tests/Scripts.test.tsx b/packages/react-router/tests/Scripts.test.tsx
index 315b3e528e..ddd11cf1a7 100644
--- a/packages/react-router/tests/Scripts.test.tsx
+++ b/packages/react-router/tests/Scripts.test.tsx
@@ -217,7 +217,7 @@ describe('ssr HeadContent', () => {
       <RouterProvider router={router} />,
     )
     expect(html).toEqual(
-      `<title>Index</title><meta name="image" content="image.jpg"/><meta property="og:description" content="Root description"/><meta name="description" content="Index"/><meta name="last-modified" content="2021-10-10"/><meta property="og:image" content="index-image.jpg"/>`,
+      `<title>Index</title><meta name="image" content="image.jpg"/><meta property="og:description" content="Root description"/><meta name="description" content="Index"/><meta name="last-modified" content="2021-10-10"/><meta property="og:image" content="index-image.jpg"/><link rel="stylesheet" href="/@tanstack-start/styles.css?routes=__root__%2C%2F" data-tanstack-start-dev-styles="true"/>`,
     )
   })
 })
