SSR & SSG
toned-styles supports SSR and static site generation out of the box. The recommended approach uses the @toned/core/vite plugin to generate CSS at build time, eliminating the need for manual CSS collection on the server.
Vite Plugin (Recommended)
The toned Vite plugin generates all token CSS via a virtual module. In production it becomes a static .css file in the bundle.
1. Vite Config
// vite.config.ts
import toned from '@toned/core/vite'
import { system } from '@toned/systems/base'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [toned({ system }), react()],
})2. Config File
Import the virtual module in your toned.config.ts:
// toned.config.ts
import '@toned/themes/shadcn/config.css'
import 'virtual:toned.css'
import { defineConfig, setConfig } from '@toned/core'
import reactConfig from '@toned/react/react-web'
export default setConfig(
defineConfig({
...reactConfig,
useClassName: true,
useMedia: true,
mediaMode: 'css',
}),
)For TypeScript, add a type reference for the virtual module:
// env.d.ts
/// <reference types="@toned/core/vite/client" />3. Server Entry
The server entry only needs to render the app — the plugin handles CSS:
// src/entry-server.tsx
import '../toned.config.ts'
import { renderToString } from 'react-dom/server'
export async function render(url: string) {
const html = renderToString(<App url={url} />)
return html
}4. Client Hydration
// src/main.tsx
import '../toned.config.ts'
import { StrictMode } from 'react'
import { createRoot, hydrateRoot } from 'react-dom/client'
import { App } from './App.tsx'
const app = (
<StrictMode>
<App />
</StrictMode>
)
const rootEl = document.getElementById('root')!
if (rootEl.firstElementChild) {
hydrateRoot(rootEl, app)
} else {
createRoot(rootEl).render(app)
}5. Prerendering (SSG)
For static sites, prerender routes at build time. The production HTML already includes a <link rel="stylesheet"> to the bundled CSS, so the prerender script only injects rendered HTML:
// prerender.js
import fs from 'node:fs'
import path from 'node:path'
const routes = ['/', '/about', '/docs/getting-started']
async function prerender() {
const template = fs.readFileSync('dist/client/index.html', 'utf-8')
const { render } = await import('./dist/server/entry-server.js')
for (const url of routes) {
const appHtml = await render(url)
const html = template.replace('<!--app-html-->', appHtml)
const filePath = url === '/'
? 'dist/client/index.html'
: `dist/client${url}.html`
fs.mkdirSync(path.dirname(filePath), { recursive: true })
fs.writeFileSync(filePath, html)
}
}
prerender()Alternative: Runtime inject()
If you are not using Vite, you can use inject() to generate CSS at runtime:
// toned.config.ts (runtime approach)
import '@toned/themes/shadcn/config.css'
import { defineConfig, setConfig } from '@toned/core'
import { inject } from '@toned/core/dom'
import reactConfig from '@toned/react/react-web'
import { system } from '@toned/systems/base'
inject(system) // inserts <style> tag in the DOM
export default setConfig(
defineConfig({ ...reactConfig, useClassName: true, useMedia: true, mediaMode: 'css' }),
)With this approach, use generate(system) in your server entry to collect CSS as a string and inject it into the HTML template manually.
Caveats
Config must load before rendering -- Import toned.config.ts at the top of your server entry so setConfig runs before any useStyles call.
Same config on both sides -- The server and client must run the same configuration. Any differences will cause hydration mismatches.