zudo-doc
GitHub repository

Type to search...

to open search from anywhere

Design Token Panel

Created Apr 22, 2026Updated Aug 5, 2026Takeshi Takatsudo

Interactive tabbed panel for live-tweaking spacing, font, size, and color tokens — plus a JSON export/import workflow you can hand to an AI.

The Design Token Panel is an in-page editor for every design token the theme ships with. It replaces the old single-purpose Color Tweak Panel with a tabbed UI that covers five token families — Palette, Color, Font, Spacing, and Size — and adds a unified JSON export/import workflow so you can round-trip a whole design through an AI assistant and back.

Enabling the Panel

Set designTokenPanel to true in zfb.config.ts:

zfb.config.ts
export default defineConfig(
  zudoDoc({
    designTokenPanel: true,
    // ...
  }),
);

When enabled, a palette icon appears in the header (to the left of the search icon). Clicking it toggles the panel open/closed; the same keyboard shortcut that opened the old Color Tweak Panel still works.

Note

The config field is designTokenPanel — set it to true to enable the panel. No host config file is required: the panel uses the package-default builder (@takazudo/zudo-doc/design-token-panel-config), derived from the shipped token manifest and the bundled color schemes, unless you point designTokenPanelConfigModule at your own (see Customizing the panel below).

Customizing the panel

By default designTokenPanel: true needs no host config file — the injected panel uses the package-default builder (@takazudo/zudo-doc/design-token-panel-config), which derives the Palette/Color tabs from the bundled color schemes and the Font/Spacing/Size tabs from the shipped token manifest.

To ship a fully custom panel (your own token manifest, your own color schemes), write a module exporting a named buildDesignTokenPanelConfig(mode: "light" | "dark") builder and point designTokenPanelConfigModule at it — a project-root-relative path, mirroring the chromeBindingsModule contract:

zfb.config.ts
export default defineConfig(
  zudoDoc({
    designTokenPanel: true,
    designTokenPanelConfigModule: "./src/config/design-token-panel-config.ts",
  }),
);

This showcase uses the package-owned bootstrap and package-default builder. Set designTokenPanelConfigModule only when the auto-injected bootstrap should load a custom mode-scoped builder instead.

The Five Tabs

The panel is divided into five tabs, each focused on a single token family. Tabs expose nothing but the tokens they own — you never have to scroll past colors to reach spacing.

Palette

The Palette tab surfaces the three shared OKLCH ramps that every color scheme is built from (see Color):

  • Base — 5 stops (--palette-base-0--palette-base-4), index 0 = lightest

  • Accent — 3 stops (--palette-accent-0--palette-accent-2)

  • State — 4 named roles: danger, success, warning, info (--palette-state-{role})

Each stop renders as a swatch on zdtp's native OKLCH lightness/chroma/hue curve editor. Dragging a stop updates every semantic role built from it live — there is no separate "Scheme…" preset dropdown; the ramps themselves are the editable source of truth. See Color Scheme Preview for a walkthrough.

Color

The Color tab is a semantic→ramp editor: 27 rows — the 4 base roles (bg, fg, selection-bg, selection-fg) plus the 23 --zd-* semantic roles — each wired to a stop on the Palette tab's ramps, or, for a handful of roles, a literal OKLCH override. The tab carries no ramp of its own; it references the Palette tab's Base / Accent / State tiers, so editing a ramp stop there recolors every row that points at it.

Ref rows — grouped ramp dropdowns

Most rows render as a <select> with Base / Accent / State <optgroup>s, one option per stop on the Palette tab's ramps. Picking an option wires the row's --zd-* custom property to var(--palette-{tier}-{n}) — a live CSS variable reference, not a resolved color — so dragging the referenced stop's L/C/H curve on the Palette tab updates the page immediately, with no need to re-pick anything on the Color tab.

Every ref row also carries a "Literal…" entry at the bottom of its dropdown, for detaching the row from the ramp and pinning a one-off color instead.

Literal rows — per-mode AA-tuned overrides

A handful of roles can't be satisfied by a shared ramp stop in both modes (see Per-mode literal overrides) and render as literal rows by default: a Literal…-selected <select> plus an editable OKLCH swatch. Which roles start as literal is scheme-defined — Default Light currently has 7 (accentHover, success, danger, warning, info, matchedKeywordBg, matchedKeywordFg), Default Dark has 3 (danger, matchedKeywordBg, matchedKeywordFg); danger is literal in both.

A literal row's swatch has a Per-mode checkbox. Turning it on reveals two independent OKLCH fields (Light / Dark) instead of one; the applied value becomes light-dark(<light-value>, <dark-value>) and the panel sets color-scheme: light dark on the applied root so the browser resolves the correct arm for the active mode.

Matched-keyword tokens

Two semantic color tokens drive how matched keywords are highlighted, rendered as literal rows (shared between light and dark — see Per-mode literal overrides) and consumed by the search widget at pages/lib/_search-widget.tsx to style hits in the search UI.

  • matchedKeywordBg — background color painted behind a matched keyword.

  • matchedKeywordFg — foreground (text) color of the matched keyword on top of that background.

Mode-scoped editing model

The Color tab always edits the wiring for whichever mode — light or dark — is currently active, seeded from that mode's ColorScheme.map in the package's @takazudo/zudo-doc/color-schemes-defaults (or your own colorSchemes override, if set — see Color).

  • Toggling light/dark re-seeds the tab's ref/literal defaults to the new mode's wiring (e.g. bg flips base-0base-4) — see Implementation for the destroy+reconfigure mechanism behind this.

  • Palette, Spacing, Font, and Size tweaks carry over the toggle untouched. Those tabs are mode-independent, so a ramp-curve edit or a spacing-slider change survives light↔dark exactly as before.

  • A saved Color-tab override is scheme- and mode-scoped. The package-default builder pairs Default Light and Default Dark in panelSettings.colorMode, so a saved Color override is restored only for its active scheme identity. An override made in one mode does not repaint the other; the Palette, Spacing, Font, and Size tabs remain shared across the toggle.

Known limitations

  • Config-level per-mode literal defaults aren't possible yet. A row's manifest default is a single string, so AA-tuned per-mode literals (like danger) are seeded from whichever mode is active at reconfigure time rather than declared as a {light, dark} pair up front. Runtime per-mode editing (the Per-mode checkbox above) is unaffected — this only limits what a config can declare as a starting default. Tracked upstream as zdtp#499.

  • Ramp-option previews in the grouped dropdowns are static. Each <option> shows the ramp stop's manifest-default color, not a live-updated swatch — if you've tweaked a stop on the Palette tab, the dropdown list still shows its original color next to the label. The applied color is still correct; only the picker's preview lags.

  • A pre-3.0 saved envelope may render broken. Before this ramp-native rewire, saved Color-tab overrides were stored as palette-index integers. Loaded against the current config, an index mapping resolves through a zdtp hydration path that currently falls back to solid black rather than the new defaults (upstream bug zdtp#497). If any semantic tokens render black after this update, click Reset all once to clear the stale mapping.

Spacing

Sliders for the horizontal (hsp-*) and vertical (vsp-*) spacing scale. Each step (3xs2xl) has its own slider with a live preview strip that shrinks and grows as you drag. Values are clamped to sensible minimums so layouts never collapse.

Font

Controls for the typography scale:

  • Font family selector (stack preset or custom)

  • Raw scale tokens (2xs2xl) via numeric text inputs with CSS-value sanitization

  • Semantic role mapping (caption, body, heading, …) that maps each role back to a scale step

The font family input accepts any valid CSS font-family value, including multi-stack fallbacks. Invalid values (unbalanced quotes, raw JavaScript, etc.) are rejected on change so pasted snippets cannot break the page.

Size

Pill-style sliders for the semantic icon-size tokens (icon-xsicon-lg) and any component dimensions that graduated from arbitrary values to semantic tokens. Each slider snaps to the px value expected by the two-tier size strategy.

Keyboard Accessibility

The tab strip implements the standard WAI-ARIA tabs pattern:

KeyAction
/ Move focus to the previous / next tab
HomeFocus the first tab
EndFocus the last tab
Enter / SpaceActivate the focused tab

Tabs use automatic activation — moving focus with the arrow keys also switches the panel. Within each tab, all sliders, selects, and text inputs are reachable via Tab, and every control exposes a visible focus ring.

JSON Export — diff-only by default

Click Export in the panel header to open the export modal. The modal shows a single JSON document describing the current state of every tab, wrapped in a tabs object keyed by tab id.

By default the export is a diff against the defaults — only tokens you have actually changed are included. Untouched tabs are omitted entirely, and within a changed tab only the individual tokens you edited are kept. The result is small, easy to diff, and stable enough to paste into a commit.

{
  "$schema": "zudo-design-tokens/v3",
  "exportedAt": "2026-07-05T12:00:00.000Z",
  "tabs": {
    "palette": {
      "raw": {
        "--palette-accent-1": "oklch(0.700 0.158 62)"
      }
    },
    "color": {
      "semantic": {
        "--zd-bg": { "ref": { "tab": "palette", "tier": "base", "item": "base-3" } },
        "--zd-danger": {
          "literal": { "light": "oklch(.505 .170 25)", "dark": "oklch(.655 .170 25)" }
        }
      }
    },
    "spacing": {
      "raw": { "--spacing-hsp-md": "1.25rem" }
    }
  }
}

$schema is zudo-design-tokens/v2 while every changed value is a plain string or palette-index number, and upgrades automatically to /v3 the moment any Color-tab row holds a {ref} or {literal} object — which, under the mode-scoped model, is normal for a real zudo-doc export. ref points at a Palette-tab ramp stop ({tab, tier, item}); literal is either a single OKLCH string or, for a Per-mode row, a {light, dark} pair. Re-importing via Load… round-trips both shapes cleanly.

Flip the Show defaults too toggle in the modal header to emit the full token tree (every tab, every token, including untouched defaults). Use this form when you want a complete snapshot to hand to a designer or AI that does not know your theme.

Load-from-JSON

The Load… button opens the import modal. Paste a JSON document — either from an earlier export or one produced by an AI — and the panel:

  1. Validates the shape (version, known keys, well-formed values).

  2. Reports any unknown keys or malformed values as non-fatal warnings.

  3. Merges the document over the current defaults. Missing tokens keep their defaults, explicit tokens override them.

  4. Persists the result to localStorage and re-applies every CSS custom property immediately.

Invalid JSON is rejected with a specific error message; partial imports (e.g. a JSON that only changes spacing.hsp.md) are a first-class use case.

AI Workflow

The diff-only export and Load-from-JSON together enable a simple round-trip with any chat-based AI:

  1. Tweak anything you want to hand off — or leave the panel untouched.

  2. Export the diff and copy the JSON.

  3. Paste the JSON into an AI chat with a natural-language request, for example:

    Here is my current design token diff. Rework this into a warmer, higher-contrast typographic scale while keeping the color palette untouched, and return only the JSON.

  4. Paste the AI's response into Load….

  5. The panel validates, merges, and applies the new tokens instantly. Hit Reset all in the panel header to roll back if you don't like the result.

Because the default export is a diff, the AI only sees the tokens you care about — it doesn't have to reason around dozens of unchanged defaults. Turn on Show defaults too only when the AI needs the complete picture (for example, producing a new theme from scratch).

Persistence

State is persisted to localStorage under the key zudo-doc-tweak-state-v4. The v4 envelope is a single object holding every tab's state side-by-side (color, spacing, typography, size, and any generic tabs). Its color state is keyed by active scheme identity, so light and dark scheme tweaks can be restored independently.

Clicking Reset all in the panel header clears the saved panel overrides and restores the configured defaults. The installed @takazudo/zdtp package owns the storage format and lifecycle; zudo-doc does not parse or rewrite persisted envelopes itself.

Switching the light/dark theme does not delete your saved tweaks, and an open panel stays open across the toggle. The theme toggle dispatches a color-scheme-changed event; the host destroys and reconfigures the panel for the new mode (see Implementation) rather than patching it in place, so the Color tab's ref/literal defaults follow the newly active mode and any saved Color override is restored for that mode's scheme identity. Palette, Spacing, Font, and Size tweaks carry over untouched. See Mode-scoped editing model.

Storage prefix continuity guarantee

The storage prefix zudo-doc-tweak is locked in the package-default builder (@takazudo/zudo-doc/design-token-panel-config, which this showcase consumes directly) and will not change. Persistence details beyond that prefix belong to @takazudo/zdtp, not to a repository-owned zudo-doc compatibility layer.

Implementation

The panel UI is provided by the @takazudo/zdtp (zdtp) npm package. The package-owned DesignTokenPanelBootstrap island and its wiring mechanism — calling configurePanel, hooking zfb's navigation lifecycle, and the mode-scoped destroy+reconfigure described below — live in @takazudo/zudo-doc/design-token-panel-bootstrap. The island calls bootstrapDesignTokenPanel(buildDesignTokenPanelConfig), where buildDesignTokenPanelConfig is a (mode) => PanelConfig builder — this is what lets the Color tab follow light/dark. The panel self-mounts as a side effect once the island hydrates.

Lazy loading

@takazudo/zdtp is not part of the initial page bundle. bootstrapDesignTokenPanel only registers a small interim click listener at mount; the actual import("@takazudo/zdtp") — and the configurePanel call that follows it — happens the first time the header trigger dispatches toggle-design-token-panel. Clicks that land before the import resolves are queued and coalesced into a single net show/hide once configure finishes, so a rapid double-click never fires it twice.

A returning visitor with saved state doesn't wait for that click. On mount, the bootstrap probes localStorage under the active (pack-scoped) storage prefix and loads zdtp immediately — before any interaction — when any of the following is true:

  • the panel was left open (${prefix}-open is "1"),

  • a token override was saved (any ${prefix}-state* key exists), or

  • an owner-mode flag is set (${prefix}:autoload, ${prefix}-elpath-enabled, or ${prefix}-domtweaker-enabled).

The ${prefix}-open and owner-mode checks are exact-key lookups; only the ${prefix}-state* check walks every key currently in localStorage (there is no native prefix-query API), but it only ever treats a key as a hit when it starts with the exact active prefix's stem — so it can't false-positive on an unrelated app's own *-state keys, even though it does look at every key name to find one. Switching to a theme pack whose namespace holds saved state triggers the same eager load.

Until zdtp has loaded, by either path, window.zdtp (the console API zdtp installs once it configures) does not exist. Once loaded it stays loaded — its own listener takes over the toggle event directly — for the rest of the page's life; only a full page load returns to this lazy state.

On every color-scheme-changed event (dispatched by the theme toggle), the bootstrap reads the new mode and coalesces rapid toggles onto a single macrotask, then calls handle.destroy() followed by configurePanel(buildDesignTokenPanelConfig(mode)) — zdtp's configurePanel throws if re-run in place with a structurally different config, so destroy-then-reconfigure is the only supported way to swap the Color tab's mode-scoped defaults. If the panel was open, it's re-mounted open.

The header trigger button dispatches the toggle-design-token-panel custom event on window. zdtp listens for this event natively, so no additional wiring is needed when the trigger is customised through settings.headerRightItems.

Settings Reference

SettingTypeDescription
designTokenPanelbooleanEnables the panel. Default false.
designTokenPanelConfigModulestringProject-root-relative path to a module exporting buildDesignTokenPanelConfig(mode) for a fully custom panel. Default undefined (package-default builder is used).

The default tokens themselves — spacing scale, font scale, icon sizes, and palette aliases — ship in @takazudo/zudo-doc/theme.css. A scaffold's later src/styles/global.css @theme block is an override point; for color ramps and mappings, use the package's @takazudo/zudo-doc/color-schemes-defaults or your own colorSchemes override (see Color). The panel only tweaks the in-browser copy of those tokens; it never writes to source files.

Tip

When you've found a combination you like, export the diff and paste it into a commit message or design doc — it's the smallest reproducible description of the change you just made.

Revision History

Takeshi TakatsudoCreated: 2026-04-23T02:37:52+09:00Updated: 2026-08-06T08:05:09+09:00

AI Assistant

Ask a question about the documentation.

Preview theme

Loading theme previews…