TanStack Start supports the CSS patterns your bundler supports, and adds SSR-aware route asset discovery on top.
Use this guide to choose how to import CSS in a React Start app, and how to configure production CSS behavior such as SSR stylesheet links, Early Hints, and CSS inlining.
Start handles CSS differently depending on how you import it.
| Pattern | Use it when | SSR behavior | Production features |
|---|---|---|---|
| import css from './app.css?url' | You want to put a stylesheet URL in route head() | Rendered from head().links | Dynamic Early Hints |
| import './global.css' | You want global CSS attached to a route chunk | Discovered from the Start manifest for matched routes | Static Early Hints, transformAssets, CSS inlining |
| import styles from './card.module.css' | You want scoped class names attached to a route chunk | Discovered from the Start manifest for matched routes | Static Early Hints, transformAssets, CSS inlining |
Use ?url when the stylesheet is part of your route head output. Use side-effect CSS imports or CSS modules when you want Start to treat the generated stylesheet as a route asset.
Import CSS with ?url when you want the bundler to return the emitted stylesheet URL and you want to render the <link rel="stylesheet"> yourself.
// src/routes/__root.tsx
/// <reference types="vite/client" />
import { createRootRoute } from '@tanstack/react-router'
import appCss from '../styles/app.css?url'
export const Route = createRootRoute({
head: () => ({
links: [{ rel: 'stylesheet', href: appCss }],
}),
})// src/routes/__root.tsx
/// <reference types="vite/client" />
import { createRootRoute } from '@tanstack/react-router'
import appCss from '../styles/app.css?url'
export const Route = createRootRoute({
head: () => ({
links: [{ rel: 'stylesheet', href: appCss }],
}),
})This pattern is useful for explicit global stylesheets, especially when you already want the stylesheet to be part of route head() output. The CSS file is emitted by the bundler, and HeadContent places the stylesheet link in the final document.
?url stylesheet links are route head output, not Start manifest-managed stylesheet assets. Because of that:
Use this pattern when explicit control of the route head is more important than Start's manifest-managed CSS features.
Import CSS without assigning it when you want global selectors, global class names, or CSS custom properties to be bundled with a route or component module.
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import '../styles/global.css'
export const Route = createFileRoute('/')({
component: Home,
})
function Home() {
return <div className="global-container">Global CSS</div>
}// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import '../styles/global.css'
export const Route = createFileRoute('/')({
component: Home,
})
function Home() {
return <div className="global-container">Global CSS</div>
}The class names remain global. Start discovers the generated CSS asset from the client build and attaches it to the matching route manifest entry. During SSR, HeadContent renders stylesheet links for the matched route tree, so the page is styled before hydration.
Where you import the CSS controls when it is loaded:
CSS imported from an async component loads with that async chunk. It is not part of Start's static route manifest for the initial route match, so HeadContent does not render it as an initial SSR stylesheet link, and it is not available for static Early Hints or CSS inlining.
Side-effect CSS imports are a good fit for app-wide CSS resets, design tokens, global utility classes, and route-level global styles.
CSS modules work like side-effect CSS imports from Start's route asset discovery perspective, but the class names are scoped by the bundler.
// src/routes/modules.tsx
import { createFileRoute } from '@tanstack/react-router'
import styles from '../styles/card.module.css'
export const Route = createFileRoute('/modules')({
component: Modules,
})
function Modules() {
return <div className={styles.card}>Scoped CSS module</div>
}// src/routes/modules.tsx
import { createFileRoute } from '@tanstack/react-router'
import styles from '../styles/card.module.css'
export const Route = createFileRoute('/modules')({
component: Modules,
})
function Modules() {
return <div className={styles.card}>Scoped CSS module</div>
}The generated stylesheet is discovered from the route chunk graph, linked during SSR for matched routes, and loaded during client navigation when the route chunk loads.
Use CSS modules for route-local or component-local styling when you want scoped class names and Start-managed stylesheet assets.
The import pattern you choose controls when Start can see the stylesheet.
CSS from side-effect imports and CSS modules is discovered at build time. Start inspects the client build output, records CSS emitted for route chunks, and uses that manifest during SSR. Because Start already knows about these stylesheets before route loaders run, they can be sent in the static Early Hints phase as rel=preload; as=style links.
CSS imported with ?url is different. Start sees that stylesheet only when route head() returns the link, which happens after router.load(). These links can still be sent as Early Hints, but only in the dynamic phase with other supported route head().links entries.
Use this rule of thumb:
See the Early Hints guide for the callback and response header APIs.
Experimental: CSS inlining is experimental and subject to change.
CSS inlining embeds Start manifest-managed route CSS directly into the server-rendered HTML response for production builds. This can improve the first render by avoiding blocking stylesheet requests for the CSS needed by the initial route match.
Enable it with server.build.inlineCss in the Start plugin options. Passing true is shorthand for { enabled: true, transformAssets: false }.
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
export default defineConfig({
plugins: [
tanstackStart({
server: {
build: {
inlineCss: true,
},
},
}),
],
})// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
export default defineConfig({
plugins: [
tanstackStart({
server: {
build: {
inlineCss: true,
},
},
}),
],
})This option only affects production builds. Development mode keeps using Start's normal development CSS handling.
CSS inlining applies to CSS discovered from side-effect imports and CSS modules, because those stylesheets are Start manifest assets. It does not inline CSS imported with ?url and returned from head().links, because those links are dynamic route head output rather than manifest-managed stylesheet assets.
When CSS inlining is enabled, Start still emits CSS files in the client build. It only changes the SSR document: stylesheet links managed by the Start manifest are replaced with a single inline <style> tag for the matched routes. The inline style is preserved during hydration to avoid duplicate stylesheet links and hydration mismatches.
You can control inlining per request in your server entry. This only affects builds created with server.build.inlineCss enabled.
// src/server.ts
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/solid-start/server'
import { createServerEntry } from '@tanstack/solid-start/server-entry'
const handler = createStartHandler({
handler: defaultStreamHandler,
inlineCss: ({ request }) => request.headers.get('x-inline-css') !== 'false',
})
export default createServerEntry({ fetch: handler })// src/server.ts
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/solid-start/server'
import { createServerEntry } from '@tanstack/solid-start/server-entry'
const handler = createStartHandler({
handler: defaultStreamHandler,
inlineCss: ({ request }) => request.headers.get('x-inline-css') !== 'false',
})
export default createServerEntry({ fetch: handler })For custom runtime wrappers, handler(request, { inlineCss }) overrides the handler-level inlineCss setting for that request.
export default createServerEntry({
fetch(request) {
return handler(request, {
inlineCss: request.headers.get('x-inline-css') !== 'false',
})
},
})export default createServerEntry({
fetch(request) {
return handler(request, {
inlineCss: request.headers.get('x-inline-css') !== 'false',
})
},
})If an inlined stylesheet contains relative url(...) or @import references, Start rebases them relative to the emitted CSS asset URL before embedding the CSS.
For example, when the generated stylesheet is served from /_build/assets/dashboard.css:
.card {
background-image: url('./dot.svg');
}.card {
background-image: url('./dot.svg');
}Start embeds it as:
.card {
background-image: url(/_build/assets/dot.svg);
}.card {
background-image: url(/_build/assets/dot.svg);
}Root-relative URLs are left unchanged at build time. Absolute URLs, protocol-relative URLs, data URLs, and hash references are also left unchanged.
If you need to rewrite CSS-internal URLs at runtime, such as prepending a CDN origin to fonts or background images, opt into CSS URL templates with server.build.inlineCss: { enabled: true, transformAssets: true }. See Transform URLs Inside Inlined CSS for the detailed transformAssets behavior.
CSS inlining is useful when the matched route CSS is small enough that putting it in the HTML is cheaper than an extra stylesheet request. It can be less effective when your initial route loads a large global stylesheet that would otherwise be cached as a separate file.
Consider these tradeoffs before enabling it:
Use CSS inlining when the reduced request overhead is worth the larger HTML response for your deployment and route structure.
Use these options together based on what you need in production.
| Need | Use |
|---|---|
| Explicit stylesheet links in route head | ?url imports returned from head().links |
| SSR links for route CSS | Side-effect imports or CSS modules |
| Earliest CSS Early Hints | Side-effect imports or CSS modules with static hints |
| Redirect-safe stylesheet Early Hints | ?url imports with dynamic hints |
| Fewer blocking CSS requests on first load | server.build.inlineCss |
| Runtime CDN rewriting for Start assets | transformAssets |