Tokens and preview CSS
Why the component previews compile a standalone stylesheet instead of sharing the host's own bundle.
Every component preview in the catalog is rendered against its own, separately compiled stylesheet — not the documentation site's own CSS bundle. This page explains why that separation exists and how it's kept intact.
The problem: three token worlds meet at one host
A styleguide host has three separate color-token worlds:
| World | Owns | Where it applies |
|---|---|---|
| zudo-doc | --zd-* raw and semantic roles | The documentation shell and its doc chrome |
| zudo-sg | --sg-* raw engine-chrome roles | Catalog chrome, code panels, workbench controls, and token controls |
| The host's component library | Its own --color-* custom properties and wind token mappings | The components rendered in preview documents |
Keep those worlds separate. A host component token such as --color-accent must not be the engine's source of truth, and a doc-chrome edit must not recolor a component preview. The standalone preview compile below provides the isolation for the component library; the engine's raw namespace provides the isolation for catalog chrome.
The fix: a standalone compile, one URL
The engine compiles the host-supplied preview stylesheet entry (previewStyles in zudo-sg.config.mjs) independently by invoking zfb's css command with the host's composed wind configuration and component-source roots. The result is served — and, at build time, written — under one dedicated URL, / by default (configurable via previewCssUrl). The standalone compile keeps host chrome out of the preview document.
Because zfb injects that one global stylesheet link into every route with no per-route opt-out, the compiler doesn't just compile the preview CSS in isolation — it also rewrites every top-level :root { … } and :root, :host { … } block in the compiled output to :root[data-sg-preview-doc] { … } (and :root[data-sg-preview-doc], :host { … }). The previewed document's own <html> element carries that data-sg-preview-doc attribute, so the previewed tokens win on specificity over the host's colliding custom properties without needing the host bundle to be touched at all.
Engine chrome has an order-proof override hook
The engine's chrome colors are the eleven plain custom properties --sg-bg, --sg-fg, --sg-surface, --sg-surface-2, --sg-border, --sg-border-strong, --sg-muted, --sg-accent, --sg-on-accent, --sg-focus, and --sg-success. Their defaults live in a zero-specificity :where(:root) block at the top of @takazudo/. The engine maps matching sg-* wind tokens to these properties; the raw namespace stays separate from both zudo-doc's --zd-* tokens and the host component library's --color-* properties.
Hosts can retheme the engine with an ordinary, unlayered root override:
:root {
--sg-border: oklch(0.72 0.02 65);
--sg-surface-2: oklch(0.94 0.01 65);
}Because the engine defaults have zero specificity, this hook wins whether the override appears before or after @takazudo/. Keep the override unlayered when using cascade layers; an earlier layer intentionally loses to unlayered engine CSS. The hook is independent of the host component library's wind tokens.
If a host previously themed engine chrome by setting bare --color-border, migrate that override to --sg-border (and migrate any other chrome role to its matching --sg-* role). Bare --color-border remains available for the host's own component library, but it is no longer an engine-chrome hook.
Code panel editor colors
The code panel's editor surface — the read-only code view and the editable Live CSS editor — is themed by sixteen plain custom properties: --sg-code-bg, --sg-code-fg, --sg-code-gutter-bg, --sg-code-gutter-fg, --sg-code-active-line-bg, --sg-code-selection-bg, --sg-code-cursor, --sg-code-keyword, --sg-code-string, --sg-code-number, --sg-code-comment, --sg-code-function, --sg-code-property, --sg-code-type, --sg-code-tag, and --sg-code-attribute.
Their defaults use light-dark(), chosen to hold WCAG AA contrast (≥ 4.5:1) against --sg-code-bg in both color schemes, and they follow the page's active scheme instead of a fixed palette. This requires the host to import @takazudo/ and set the CSS color-scheme property on an ancestor element; zudo-doc already does this from its data-theme attribute, so hosts built on zudo-doc get correct scheme-following for free.
Hosts can override any of the sixteen roles with an ordinary, unlayered root rule, the same as the eleven chrome roles above:
:root {
--sg-code-bg: #1e1e2e;
--sg-code-keyword: #cba6f7;
}Any override must keep ≥ 4.5:1 contrast for plain text (--sg-code-fg against --sg-code-bg) and for each syntax color against --sg-code-bg — the engine does not validate contrast for a host-supplied palette.
The editable Live CSS editor highlights its active line with --sg-code-active-line-bg, applied to both the line content and its gutter cell.
SourceEditor's extensions prop
SourceEditor — for hosts that mount the editor directly instead of through CodePanel — accepts a CodeMirror extensions prop. Host extensions are appended after the engine's own theme extension, so they win over it; the engine theme is intentionally low precedence. The prop is read once, at mount — change it by remounting with a new key, not by passing a new array on re-render. CodePanel does not expose this prop itself, because island props must be JSON-serializable and CodeMirror extensions are not.
Import order still matters for host component tokens
zudo-doc 6 publishes theme.css with its design properties and wind.json with its utility candidates. theme.css does not reset host color properties; the package has one theme stylesheet for this contract. Import it before the host's own component token stylesheet, then import component styles. If a host overrides a property that zudo-doc also declares, put its :root override after theme.css so normal source order gives the host value precedence. This import order concerns host component tokens; the --sg-* engine namespace is independent.
Dev and build both serve it
In dev, the preview plugin registers its unprefixed route and zfb mounts it under the configured base. It recompiles whenever an input changes — the stylesheet entry, files it imports, host configuration or wind manifests, and the component source files scanned for utility candidates.
In a production build, the compiled stylesheet is written directly under the URL path within the build output, not nested inside the base segment — the same placement convention the framework uses for its own generated assets.
What the host keeps
The host still owns and maintains its own stylesheet for its own chrome — nothing about this changes how the documentation site styles itself. What the host gives up is trying to render component previews through that same stylesheet: the preview CSS entry it points the engine at is a dedicated, standalone entry (in this project, src/) that pulls in the component library's own token and utility layers, kept deliberately separate from the host's chrome stylesheet.
Note
A previous, pre-engine version of this host kept a hand-maintained duplicate of the component library's tokens re-asserted inside its own global stylesheet, specifically to make previews look right. That workaround is gone — the standalone compile described above replaces it.