Skip to content

Guides

Server rendering and Server Components

Toned works with server rendering, static generation and React Server Components. CSS is generated at build time, so the server sends complete markup with nothing to inject, and a Server Component can resolve its styles without hooks or client JavaScript.

  • SSR and static generation: the same sheets, renderer inputs and theme produce the server markup and the first client render, so hydration matches.
  • Server Components: renderer.resolve(sheet) returns plain props for each part. No hook, context or 'use client' is needed.
  • Client Components: @toned/react declares its own client boundary, so createElements families can be rendered from a Server Component.

Vite delivery

Pass the complete system returned by defineSystem and every sheet, including lazy routes. The raw token dictionary alone does not carry a stylesheet inventory or system namespace.

system.ts and styles.ts are the modules from Getting Started.

vite.config.ts
import toned from '@toned/core/vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import { buttonStyles } from './styles.ts'
import { ui } from './system.ts'

export default defineConfig({
  plugins: [toned({
    system: ui,
    sheets: [buttonStyles],
    inputs: ['system.ts', 'styles.ts'],
  }), react()],
})

Import virtual:toned.css in the application and use virtual:toned.manifest to create the renderer. See Getting Started for the complete provider setup. The production CSS file must be linked from the initial HTML.

Without Vite

build-styles.ts
// Run during the application build.
import { mkdir, writeFile } from 'node:fs/promises'
import { buildStyles } from '@toned/core/build'
import { buttonStyles } from './styles.ts'
import { ui } from './system.ts'

const artifact = buildStyles(ui, { sheets: [buttonStyles] })
await mkdir('public/assets', { recursive: true })
await writeFile('public/assets/toned.css', artifact.css)
await writeFile('public/assets/toned.manifest.json', JSON.stringify(artifact.manifest))

Load that manifest into createWebRenderer and pass the renderer and web host to TonedProvider. Missing or stale build inputs are diagnosed instead of repaired by a browser-side injection fallback.

Render and hydrate

entry-server.tsx
import { renderToString } from 'react-dom/server'
import { App } from './App.tsx'
export function render() {
  return renderToString(<App />)
}
entry-client.tsx
import { hydrateRoot } from 'react-dom/client'
import { App } from './App.tsx'
const root = document.getElementById('root')
if (!root) throw new Error('Missing application root')
hydrateRoot(root, <App />)

For static generation, insert the rendered HTML into a template linking the generated stylesheet. Use matching theme values and variants for server output and hydration. Request-specific tokens belong in provider inputs, not mutations of a shared global configuration.

React Server Components

A Server Component cannot use hooks or context. Create the renderer in a server module and resolve the sheet directly: the result is plain props for each part. Variants are ordinary arguments. Hover, focus and media conditions are CSS, so they work without any client JavaScript.

SaveButton.tsx
// A Server Component: no 'use client', no hooks.
import { renderer } from './renderer.ts'
import { buttonStyles } from './styles.ts'

export function SaveButton({ size }: { size: 's' | 'm' }) {
  const s = renderer.resolve(buttonStyles, { variants: { size } })
  return (
    <button type="button" {...s.Root}>
      <span {...s.Label}>Save</span>
    </button>
  )
}
renderer.ts
import manifest from 'virtual:toned.manifest'
import { createWebRenderer } from '@toned/core/server'
import { ui } from './system.ts'

export const renderer = createWebRenderer(ui, { manifest })

Use a Client Component when the styles depend on state held in the browser. createElements families are client components and can be imported into a Server Component as they are; they need a TonedProvider above them, as in Getting Started.

LikeButton.tsx
'use client'
import { createElements } from '@toned/react'
import { useState } from 'react'
import { buttonStyles } from './styles.ts'

const S = createElements(buttonStyles)

export function LikeButton() {
  const [liked, setLiked] = useState(false)
  return (
    <S size={liked ? 'm' : 's'}>
      <S.Root as="button" type="button" onClick={() => setLiked(!liked)}>
        <S.Label as="span">{liked ? 'Liked' : 'Like'}</S.Label>
      </S.Root>
    </S>
  )
}

Pure server resolution

@toned/core/server can resolve part props without importing React, reading browser globals or mounting hosts. React SSR uses the separate React binding. Module-level createElements creates stable component identities without reading host configuration.

@toned/core/dev/inject is an explicit development helper. It is not the production or SSR delivery path.