Server-Side Rendering
Apollion is engineered for the server on purpose. Deterministic
componentIds, a styled-components SSR path validated by real SSG
consumers, and a CSS-vars surface for HTML that ships no React at all —
this page is the map of that story.
Deterministic class names
The published lib/ bakes a deterministic componentId into every styled
component at build time (an esbuild onLoad plugin running the
babel-plugin-styled-components transform with ssr: true +
fileName: true). Server and client always agree on class names — zero
hydration mismatch by construction, regardless of chunk evaluation order.
Since v5.2.0 the published artifact also sets displayName: false:
shipped classes are short (sc-<hash>-<index>, no
Foo__Bar- prefix), which matters when your server output is the
final product — static HTML, reports, e-mails — and every byte of
class repeats per node. Debugging stays readable in dev: your local
Babel adds display names to src/, not to the published artifact.
The styled-components SSR path
Collect styles with ServerStyleSheet — the pattern below is extracted
from a production SSG consumer:
import { renderToStaticMarkup } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';
import { ApollionProvider } from '@apollion-dsi/core/themes';
function renderPage(tree: React.ReactElement): string {
const sheet = new ServerStyleSheet();
try {
const body = renderToStaticMarkup(sheet.collectStyles(tree));
const styles = sheet.getStyleTags(); // <style> tags with the collected CSS
return `<!DOCTYPE html>
<html>
<head>${styles}</head>
<body>${body}</body>
</html>`;
} finally {
sheet.seal();
}
}
renderPage(
<ApollionProvider>
<App />
</ApollionProvider>,
);For hydrating apps (Next.js and friends), use renderToString instead
of renderToStaticMarkup and inject sheet.getStyleElement() into your
document — the deterministic ids guarantee the client picks up the exact
server tree. The docs website you are reading does exactly this through
Next.js.
Per-request theming
Theme resolution is pure and synchronous — createTheme runs anywhere
Node runs:
import { ApollionModeScript, readModeFromCookie } from '@apollion-dsi/core/themes/mode';
import { ApollionProvider } from '@apollion-dsi/core/themes';
// e.g. brand from the request's tenant, mode preference from the cookie the
// provider persists (`persist="cookie"`)
const preference = readModeFromCookie(request.headers.cookie) ?? 'system';
renderPage(
<>
<ApollionModeScript persist="cookie" defaultMode="system" />
<ApollionProvider
colors={tenantSeeds}
defaultMode={preference}
initialMode={preference === 'dark' ? 'dark' : 'light'}
persist="cookie"
>
<App />
</ApollionProvider>
</>,
);Two facts keep server and client honest with each other:
initialModeis the hydration contract. Server and first client render use it; the persisted/system preference is applied in a layout effect — after hydration, before paint — so React never sees a mismatch. The pre-paint script (<ApollionModeScript />/getModeScript()) writes the same<html>attributes before the first paint.- The flip is CSS, not a re-render. Every color is a
light-dark()pair and the mode is decided bycolor-schemeon<html>— class hashes are identical in both modes and the server output is byte-deterministic. Details: Dark Mode Engine.
No React on the page? Use the tokens CSS surface
For SSG output, reports or any consumer that doesn't re-bundle React, the
@apollion-dsi/tokens CLI emits the whole resolved theme as CSS custom
properties:
apollion-tokens build --config apollion.config.mjs --out dist/
# → dist/css/<variant>.css (--apollion-color-primary-base, --apollion-tx-primary,
# --apollion-color-primary-on-light, …)Link the variant stylesheet and consume the vars from any markup. This is the right layer when the runtime theme object would be dead weight — the values are identical by construction (the tokens build mirrors the core engine, guarded by parity tests).
Deterministic builds as a cache ally
apollion-tokens build is byte-identical across reruns for the same
config (no timestamps, no run ids), and each output is sha256-fingerprinted
in a build manifest. Practical consequence: your static pipeline can cache
on configHash and skip whole rebuilds, and CDN caches of the emitted CSS
bust exactly when the theme actually changes — never spuriously.
Checklist
| Scenario | Reach for |
|---|---|
| Next.js / hydrating SSR | ServerStyleSheet + renderToString + provider |
| Static HTML, reports, e-mails (SSG) | renderToStaticMarkup + short sc-* classes |
| Dark mode in static output | css/<brand>.<surface>.<dimension>.modes.css (output.cssModes) + getModeScript() |
| Markup without React | dist/css/ vars from apollion-tokens build |
| Cache/CI | build manifest configHash as the cache key |
See also
- Dark Mode Engine — mode preference, persistence,
initialMode, the pre-paint script. - Tokens output formats — the CSS/TS/JSON surfaces.
- ApollionProvider — theme/mode props.