toned-styles
Overview
Getting StartedCore Concepts
API Reference
defineSystemstylesheetvariantsuseStylesMedia Queries
Guides
React WebReact NativeThemingInteractive StylesSSR & SSG

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.