Server-side rendering
To stream a styled document, route motif's generated CSS through SSRStyleCollector and inject
the result into the response. Two patterns: a synchronous one for renderToString, and a registry
component for streaming SSR.
Sync render-to-string
For Express / Fastify / Vite SSR / any server using renderToString, create a collector per
request, run the render inside it, and embed the captured <style> block in the head.
The collected rules ship as a <style data-motif-ssr> block. On the client, motif's runtime
reads the marker, seeds its dedup set, and skips re-injecting any rule the server already emitted.
No flash; no double work.
Streaming SSR and RSC
Streaming renderers (renderToReadableStream, renderToPipeableStream) interleave async work
across requests. The default sync collector is module-scoped, which corrupts under concurrency —
two requests share the same active pointer.
The fix is one import at app startup:
That module registers an AsyncLocalStorage-backed storage for the active collector. Each
request sees its own collector; concurrent requests do not collide.
After the import, the rest of the API is the same: create a collector per request, route the
render through collector.collect(...) (or via CollectorContext, below), and emit the style tag.
Next.js App Router
App Router renders in chunks and re-flushes the head as it streams. Motif ships no built-in provider for this — the integration is a fifteen-line registry that lives in your app.
Mount it inside <body> in your root layout, with the theme provider underneath:
useServerInsertedHTML fires on every flush; _drain() clears the collector's emitted rules so
each flush only includes what is new. The dedup set stays populated, so two flushes never emit the
same class twice.
Hydration without a flash
Three things to confirm before shipping:
- The
<style data-motif-ssr>block is in the streamed HTML. Openview-source:on a server-rendered page; the block should appear in<head>(sync) or near the top of<body>(App Router). If it is not there, the collector ran but nothing was injected — check that the collector is in scope when the components render. - The theme variables block is present.
<ThemeProvider>emits a<style data-motif-themes>block of its own. Both blocks need to be in the document on first paint, or token references resolve to the browser default during hydration. prefers-color-schemelands the right theme. If you persist the user's choice inlocalStorageor a cookie, setdata-themeon<html>from the server (cookie) or via a pre-paint script in the document head (localStorage). Otherwise the first paint is whichever theme isactiveon the provider.