zudo-sg Docs

Type to search...

to open search from anywhere

Design Tokens

The three token worlds, the shared UI token pipeline, and the one live design-token panel — its header trigger, workbench button, and the /tokens dashboards.

Three token worlds

zudo-sg hosts three independent color-token systems:

  • zudo-doc's --zd-* tokens control the documentation shell: navigation, docs pages, code blocks, sidebars, and (where a host enables it) zudo-doc's own design-token panel.

  • zudo-sg's raw --sg-* tokens control the installable engine's catalog chrome, code panel, workbench, and token controls.

  • The host component library owns its own plain CSS color tokens, commonly semantic --color-* roles, for target-site components and preview iframes.

Keep those worlds separate. A styleguide chrome color scheme is not automatically the same as the target website palette, even though both can be edited from zdtp-powered panels.

UI package token contract

Consumers import the shared UI token CSS and register the package token map in their zfb wind configuration:

@layer zw-reset;
@import "@zudo-sg/demo-ui/styles/tokens.css";
@import "@zudo-sg/demo-ui/styles/colors.css";

tokens.css defines spacing, typography, radius, shadow, and related non-color custom properties. packages/demo-ui/styles/wind-tokens.mjs supplies their utility names; zudo-wind owns reset and utility generation. colors.css defines the UI package color system. Components use semantic utilities such as bg-accent, text-fg, border-border, and bg-surface-2; they do not reach into raw palette variables.

The preview stylesheet is compiled standalone

The catalog renders components in a separate same-origin preview document. The engine's preview-CSS plugin (@takazudo/zudo-sg/plugins/preview-css, composed by withZudoSg()) compiles the host's previewStyles entry with the host-installed zfb css CLI and the composed zudo-wind token map. This repository's entry, src/styles/preview-entry.css, imports the demo-ui token, color, and component CSS.

PostCSS rewrites actual :root selectors to :root[data-sg-preview-doc] without rewriting quoted selector values or :host. The iframe carries that attribute, so preview token defaults remain scoped. The stylesheet is served at previewCssUrl (default /_zudo-sg/preview.css) in development and production; the development compiler tracks imported CSS and source inputs for invalidation. The /tokens dashboards instead mount their own opaque zdtp widgets in the host.

Three-tier color strategy

The UI package follows the Three-Tier Color Strategy:

TierPurposeSource
PaletteRaw OKLCH values named --palette-{group}-{step-or-role}packages/demo-ui/styles/colors.css
Semantic--color-* roles backed by palette refs, with a few zudo-doc-style AA-tuned light-mode literalspackages/demo-ui/styles/colors.css
ComponentScoped overrides when a component truly needs themcomponent code or CSS

Palette groups used by the UI palette tab are neutral, accent, state, and one tier per business line: line-vacuum, line-process, line-laser, line-meeting, and line-beauty. neutral is the grayscale / warm-neutral ramp, accent is the amber action ramp, state contains named status colors (danger/success/warning/info), and each line-* group supplies a business line's own accent + hover pair, layered in through a [data-line="<key>"] override of the semantic --color-accent/--color-accent-hover roles below — components never bind to a line-* palette entry directly. Keeping each line in its own tier gives it an independent OKLCH curve and satisfies zdtp 0.6's named-palette prefix validation.

Semantic (Tier 2) roles built from these groups: bg, surface, surface-2, border, fg, muted, accent, accent-hover, on-accent, focus, the four state colors, and the rail-* family (a persistent dark nav surface — deliberately not a light-dark() pair, so it stays dark in both schemes).

No palette utilities

Palette variables live in plain :root and are absent from the zudo-wind utility token map, so bg-palette-* and text-palette-* utilities are not generated. Components must bind to semantic --color-* roles instead.

Doc chrome vs. engine chrome color models

zudo-doc 6 ships a ramp-native model for its own --zd-* chrome color schemes, editable through zudo-doc's built-in design-token panel (the designTokenPanel feature flag). This project keeps that flag off: the panel was host-only wiring that create-zudo-sg never scaffolds, so a component-catalog dogfooding host advertising it would mislead adopters. Nothing in --zd-* is reachable from a panel here — only the preview token panel below is live, and it edits component tokens, not chrome tokens.

The engine's own chrome roles are 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. The package declares their defaults under zero-specificity :where(:root), separate from the host utility token map. An unlayered host override wins before or after the engine stylesheet:

:root {
  --sg-border: oklch(0.72 0.02 65);
}

This is the supported engine-chrome hook. If a host previously set bare --color-border to recolor engine chrome, replace it with --sg-border (and map other chrome roles to their matching --sg-* names). Bare --color-border remains a host component-library token.

The host's component tokens are ordinary custom properties. Import the doc theme before component token overrides when their names overlap, and register only the semantic utility names the component library needs in wind.tokens. Current zudo-doc 6 does not export the old theme-no-reset.css alternative; no Tailwind @theme block or wildcard color reset is part of this pipeline.

Shared build-safe tabs (src/config/ui-token-tabs.ts)

src/config/ui-token-tabs.ts shapes the generated UI token manifest (src/config/ui-design-tokens-manifest.ts) into uiTokenTabs and dashboardPreviewOverrides. src/config/preview-token-panel-tabs.ts re-exports uiTokenTabs as the tabsModule fed to @takazudo/zudo-sg/plugins/zdtp-apply-proxy (zfb.config.ts's zdtpApplyProxy option), which the preview token panel reads at build time through its virtual:zudo-sg-preview-token-panel module. The static /tokens dashboard route is injected by the @takazudo/zudo-sg engine rather than by a host file: the engine ships its own build-safe tab-shaping module (packages/styleguide/src/token-dashboard/ui-token-tabs.ts), fed by the same manifest through the routes plugin's virtual:zudo-sg-tokens module (source: zudo-sg.config.mjs's tokens.manifestOut). The two copies stay equivalent because both derive their shaping logic from the same generated manifest and the same zudo-sg.config.mjs token configuration — see docs/adr/styleguide-engine.md for the composition contract.

Keep the host module importable by plain Node during a build. Its runtime dependency is the generated ui-design-tokens-manifest.ts; zdtp and dashboard imports are type-only, and the module contains data-shaping code without browser globals, a dev-server module, or panel setup.

src/config/__tests__/ui-token-tabs.test.ts enforces this boundary by checking the runtime import allowlist and importing the module with Node's native type stripping. It also checks the manifest mapping, the per-line palette order, the dashboard override coverage, and — via @takazudo/zdtp/testing's assertValidPanelConfig — that the panel config these tabs feed is structurally valid.

Read-only token dashboard

The /tokens route is injected by the @takazudo/zudo-sg engine (packages/styleguide/src/routes/tokens.tsx) and creates three zudo-react islands through @takazudo/zudo-sg/token-dashboard. Each island dynamically mounts the opaque Preact zdtp adapter, with a loading fallback and deterministic cleanup. Their values remain read-only declared defaults: the light and dark views use include="mode-dependent" and show only rows whose declared values differ by mode, while the shared view uses include="mode-independent" and shows every other row once. The route uses the engine's own uiDashboardTabs data (built from the manifest virtual module) and dashboardPreviewOverrides, then gives the instances these stable IDs and titles:

IDTokenDashboard propsTitle
ui-defaults-lightmode="light", chrome="light", include="mode-dependent"UI tokens — mode-dependent defaults (light)
ui-defaults-darkmode="dark", chrome="dark", include="mode-dependent"UI tokens — mode-dependent defaults (dark)
ui-defaults-sharedchrome="host", include="mode-independent"UI tokens — shared defaults (mode-independent)

The shared view follows the site theme, and all three views remain read-only inventories of @zudo-sg/demo-ui declared defaults.

The section supplies zdtp's four public light/dark background and foreground inputs from the configured site color schemes. Dashboard headers and inventory regions therefore use the site's warm-neutral palette rather than zdtp's default blue-gray chrome. These are resolved declared colors, so live panel tweaks do not alter the static dashboard palette.

Dashboard styling is delivered through the host CSS pipeline with @import "@takazudo/zdtp/dashboard/styles.css" in src/styles/global.css. This is separate from the panel's self-injected stylesheet. Loading the dashboard stylesheet through a per-page public asset would require a copy step for development and production and would introduce base-URL handling, so the public-asset alternative was rejected.

The page uses zdtp dashboards as its only token listing. A PreviewTokensButton island above the dashboards opens the preview token panel described below.

dashboardPreviewOverrides is passed through the dashboard's previewOverrides prop to make the static samples explicit. The light-dark() semantic color rows receive color samples even though their panel editor entries are text declarations, and shadow declarations receive shadow samples. previewBase: "--text-base" supplies the shared font-size base for the line-height tiers. The API accepts a tier-level base, so it cannot select a different base for every font-size-lh row; this is the deliberate compromise for those per-size leading samples.

See the upstream TokenDashboard reference and static token dashboard recipe for the component and static-rendering contracts.

The preview token panel

The styleguide ships one live design-token panel: the preview panel, defined by createPreviewTokenPanelConfig() (packages/styleguide/src/token-tweak/preview-token-panel-config.ts, in the engine, not the host). It edits the @zudo-sg/demo-ui component tokens — --color-*, --spacing-*, --font-*, --radius-*, --shadow-* — and pushes changes into the catalog's preview iframes via applySink, never into the host document's own :root.

Identity: storagePrefix "sg-preview-tweak", consoleNamespace "sgPreview" (so window.sgPreview.* targets it), toggleEvent "toggle-preview-token-panel", and autoRememberOnOpen: false — opening the panel never arms owner-mode autoload by itself; that still requires the explicit window.sgPreview.enableAutoload() opt-in.

Three ways to open it

The same panel instance opens from three places, all dispatching the same toggle-preview-token-panel event:

  • Header trigger — a sliders-glyph button (id="sg-preview-tokens-trigger", aria-label="Open component tokens panel", title="Component tokens") that withZudoSg() appends to the zudo-doc header's right-hand items, behind a headerTokenTrigger?: boolean option (default true). It ships hidden in the static HTML; an inline script reveals it only on the engine's four routes (/components, /components/[slug], /components/preview, /tokens), synced from a data-sg-engine-route marker that routes/_chrome.tsx emits in the bodyEnd slot of every engine-chrome page, re-checked on load and after every client-router navigation. A regular docs page never shows the button. Unlike the chrome-defaults headerNav/headerRightItems promotion (which only fires for a host with an empty headerNav), this trigger is unconditional — a host with its own navigation, like this showcase, still gets it. Its inline script captures clicks until the preview panel's load island registers its listener, including across a host-to-engine SPA transition.

  • Workbench "Preview tokens" button — the stacked-frames-glyph button in a component detail page's toolbar (/components/<slug>).

  • /tokens dashboard button — the PreviewTokensButton island above the read-only dashboards.

These identifiers are deliberately distinct from the retired doc-chrome trigger (id="sg-doc-tweak-trigger", event toggle-sg-doc-tweak) so any regression guard built against the old names keeps its meaning.

For a custom header trigger, render the package's previewTokenPanelCaptureScript() (@takazudo/zudo-sg/token-tweak) in a body-end <script> before PreviewTokenPanelBootstrap on host and engine routes. The helper can appear on each route without adding another listener. It replaces the older host-copied zdtp-preview-prehydrate script; existing hosts that still render that script remain compatible.

Tier preview kinds

Setting a tier's preview renders a specimen next to its rows (type-size samples, spacing bars, radius glyphs) instead of a flat row of numeric/text controls:

TabTierpreviewWhy
Fontfont-size'size'Instance-value specimens
Fontfont-size-lh'line-height' (previewBase: '--text-base')Per-size leading rows; tier-level base is a compromise
Fontfont-weight'weight'Styled from this tab's first font-family item
Fontline-height'line-height' (previewBase: '--text-base')Explicit font-size base
Fontfont-family'family'Styles specimens from this tab's first family item
Spacinghsp / vsp'bar'Both write a resolved length, not a var() reference
Sizeradius'radius'Resolved-value glyph, same reasoning as bar
SizeshadownoneFree-text tier; no preview kind matches text values
Palette / Colorall tiersnoneThese tiers render their own swatch/curve editors instead

'family' and 'weight' tiers supply no specimen of their own — per zdtp's PORTABLE-CONTRACT.md §3.2, the first item in each instead styles every 'size' and 'line-height' specimen sample rendered elsewhere in the same font tab.

The font-specimen toolbar and its Render on page control turn on together whenever a tab has the reserved id font plus at least one 'size' or 'line-height' preview tier — there is no separate flag for it.

The { kind: 'number' } opt-in for line-height tiers

preview: 'line-height' requires every item in that tier to have type.kind === 'number', a rule enforced by zdtp's assertValidPanelConfig (see the tests named above). The unitless line-height tokens (--leading-*, --text-*--line-height) would otherwise map to { kind: 'length', unit: '' } through the normal shaping path, so ui-token-tabs.ts adds a local numberKind option that emits { kind: 'number', step, unit } instead, used only for the line-height groups. This lives in the tab-shaping code, not in the generated manifest: the manifest's TokenDef.control values map onto zdtp's upstream TokenControl type, which has no 'number' member, and the manifest is --check-gated codegen (pnpm check:token-manifest) regenerated straight from CSS source, so a hand-added field there would be silently reverted at the next regen.

Apply — persist a tweak to CSS source

The panel's Apply button POSTs the current tweak diff to a dev-only endpoint that rewrites the value directly into the project's CSS source, so the change survives a hard reload and shows up as an ordinary git diff. It only works under pnpm dev (zfb dev).

The @takazudo/zudo-sg/plugins/zdtp-apply-proxy engine plugin registers a same-origin devMiddleware handler at /__zdtp/apply (no separate process, no port, no CORS), backed by @takazudo/zdtp/server's createApplyHandler. This project's zfb.config.ts wires it with routingFile: "./zdtp-panel-routing.json" and writeRoot: "./packages/demo-ui/styles" — a prefix → file routing map covering palette, color, spacing, text, font, leading, radius, and shadow, all resolving into packages/demo-ui/styles/{tokens,colors}.css. A CSS var whose prefix isn't in that map still fails the request with "Unsupported cssVar prefix" (400) — none of the tokens the panel currently exposes fall into that category. The routing map and the endpoint path both resolve to undefined outside zfb dev, so a production build never sees them.

If the source CSS files change, regenerate the manifest and commit the result:

pnpm gen:token-manifest
pnpm check:token-manifest

For the day-to-day operating guide (what each panel action does, retheming engine chrome, declared-defaults on /tokens), see the styleguide's Design Token Panel guide.

Scaffold parity

A project scaffolded with create-zudo-sg gets a working panel out of the box: bundleZdtp: true (bundling follows bundleZdtp ?? designTokenPanel, so a scaffold without it would stub the zdtp loader and the panel would reject at runtime), a tabsModule (zdtpApplyProxy: { tabsModule }, tabs-only — routingFile and writeRoot are optional together, so a fresh scaffold gets a non-inert panel without also getting a dev-only Apply write sandbox it has no sensible default path for), and a chromeBindingsModule so the panel's bootstrap island mounts on the engine's injected routes and not just the host's own /. Wiring the Apply sandbox into a scaffold is a deliberate opt-in next step, left to the adopting project.

Architecture map, not the operating manual

This page is the architecture map: CSS files are the source of truth, the generated manifest keeps the panel and the /tokens dashboards in sync, and pnpm check:token-manifest catches drift between them. For how to actually use the panel day to day, see the styleguide site's Design Token Panel guide linked above.

Host-owned token groups

The engine accepts an optional tokens.spec in zudo-sg.config.mjs. It replaces its historical token selection with ordered palette, color, spacing, font, and size groups, so a component library can expose its own CSS variables without demo naming. Every token points to a literal declaration in the configured CSS files. Group labels and order drive both the preview panel and the /tokens dashboard; palette entries retain their exact CSS variable names. The engine README has a runnable configuration example and the supported preview/kind combinations. Existing manifests without UI_TOKEN_GROUPS still use the historical layout.