Custom color cluster
Wire a non-default palette plus a semantic token table into the panel's Color tab using TabConfig with colorExtras.
The package ships zero baked-in color data. The host owns the palette,
the semantic roles, and the scheme registry. This recipe walks through
wiring a fresh Color tab for a new project using the TabConfig shape.
The Color tab is a TabConfig with the reserved id: 'color'. Its tiers
hold the palette and semantic data as TierItem arrays; the colorExtras
field carries the non-tier metadata (base roles, color schemes, panel
settings). For the authoritative shape, see
Color cluster reference.
1. Define the Color tab
Pick CSS variable names that match your stylesheet. The first palette item’s
cssVar determines the var-name template the internal bridge uses
(--myapp-palette-0 → --myapp-palette-{n}).
// src/lib/my-tabs.ts
import type { PanelConfig, ColorScheme } from '@takazudo/zdtp';
type TabConfig = PanelConfig['tabs'][number];
const defaultDark: ColorScheme = {
background: 0,
foreground: 7,
cursor: 7,
selectionBg: 8,
selectionFg: 0,
palette: [
'#1e1e2e', // p0 — base background
'#f38ba8', // p1 — red
'#a6e3a1', // p2 — green
'#f9e2af', // p3 — yellow
'#89b4fa', // p4 — blue
'#cba6f7', // p5 — magenta
'#94e2d5', // p6 — cyan
'#cdd6f4', // p7 — base foreground
'#45475a', // p8 — bright black
'#f38ba8',
'#a6e3a1',
'#f9e2af',
'#89b4fa',
'#cba6f7',
'#94e2d5',
'#bac2de',
],
shikiTheme: 'github-dark',
// Per-scheme semantic overrides. Keys must be ids from the semantic tier.
semantic: {
primary: 4,
accent: 5,
danger: 1,
success: 2,
warning: 3,
muted: 8,
},
};
export const colorTab: TabConfig = {
id: 'color',
label: 'Color',
// colorExtras carries non-tier metadata: base roles, schemes, panel settings.
colorExtras: {
id: 'myapp',
label: 'MyApp Brand',
baseRoles: {
background: '--myapp-color-bg',
foreground: '--myapp-color-fg',
},
baseDefaults: {
background: 0,
foreground: 7,
},
defaultShikiTheme: 'github-dark',
colorSchemes: {
'Default Dark': defaultDark,
},
panelSettings: {
colorScheme: 'Default Dark',
// Single-mode site. See secondary-cluster-or-disable for the
// light/dark pairing pattern.
colorMode: false,
},
},
tiers: [
{
id: 'palette',
label: 'Palette',
items: [
{ id: 'myapp-palette-0', cssVar: '--myapp-palette-0', label: 'P0', default: '#1e1e2e', type: { kind: 'color' } },
{ id: 'myapp-palette-1', cssVar: '--myapp-palette-1', label: 'P1', default: '#f38ba8', type: { kind: 'color' } },
{ id: 'myapp-palette-2', cssVar: '--myapp-palette-2', label: 'P2', default: '#a6e3a1', type: { kind: 'color' } },
{ id: 'myapp-palette-3', cssVar: '--myapp-palette-3', label: 'P3', default: '#f9e2af', type: { kind: 'color' } },
{ id: 'myapp-palette-4', cssVar: '--myapp-palette-4', label: 'P4', default: '#89b4fa', type: { kind: 'color' } },
{ id: 'myapp-palette-5', cssVar: '--myapp-palette-5', label: 'P5', default: '#cba6f7', type: { kind: 'color' } },
{ id: 'myapp-palette-6', cssVar: '--myapp-palette-6', label: 'P6', default: '#94e2d5', type: { kind: 'color' } },
{ id: 'myapp-palette-7', cssVar: '--myapp-palette-7', label: 'P7', default: '#cdd6f4', type: { kind: 'color' } },
{ id: 'myapp-palette-8', cssVar: '--myapp-palette-8', label: 'P8', default: '#45475a', type: { kind: 'color' } },
{ id: 'myapp-palette-9', cssVar: '--myapp-palette-9', label: 'P9', default: '#f38ba8', type: { kind: 'color' } },
{ id: 'myapp-palette-10', cssVar: '--myapp-palette-10', label: 'P10', default: '#a6e3a1', type: { kind: 'color' } },
{ id: 'myapp-palette-11', cssVar: '--myapp-palette-11', label: 'P11', default: '#f9e2af', type: { kind: 'color' } },
{ id: 'myapp-palette-12', cssVar: '--myapp-palette-12', label: 'P12', default: '#89b4fa', type: { kind: 'color' } },
{ id: 'myapp-palette-13', cssVar: '--myapp-palette-13', label: 'P13', default: '#cba6f7', type: { kind: 'color' } },
{ id: 'myapp-palette-14', cssVar: '--myapp-palette-14', label: 'P14', default: '#94e2d5', type: { kind: 'color' } },
{ id: 'myapp-palette-15', cssVar: '--myapp-palette-15', label: 'P15', default: '#bac2de', type: { kind: 'color' } },
],
},
{
id: 'semantic',
label: 'Semantic',
// Each item's default is the id of a palette item.
// The apply pipeline emits var(--that-palette-cssVar).
referencesTier: 'palette',
items: [
{ id: 'primary', cssVar: '--myapp-color-primary', label: 'Primary', default: 'myapp-palette-4', type: { kind: 'color' } },
{ id: 'accent', cssVar: '--myapp-color-accent', label: 'Accent', default: 'myapp-palette-5', type: { kind: 'color' } },
{ id: 'danger', cssVar: '--myapp-color-danger', label: 'Danger', default: 'myapp-palette-1', type: { kind: 'color' } },
{ id: 'success', cssVar: '--myapp-color-success', label: 'Success', default: 'myapp-palette-2', type: { kind: 'color' } },
{ id: 'warning', cssVar: '--myapp-color-warning', label: 'Warning', default: 'myapp-palette-3', type: { kind: 'color' } },
{ id: 'muted', cssVar: '--myapp-color-muted', label: 'Muted', default: 'myapp-palette-8', type: { kind: 'color' } },
],
},
],
};
export const myTabs: readonly TabConfig[] = [colorTab];
ℹ️ Info
Palette length invariant. ColorScheme.palette.length MUST equal the
number of items in the palette tier. Add or remove palette items and scheme
arrays together or the panel will reject the config at init time.
2. Plug it into configurePanel
Pass the tabs array to configurePanel. Any other tabs (spacing, font, etc.)
join the same array.
// src/lib/my-panel-config.ts
import type { PanelConfig } from '@takazudo/zdtp';
import { myTabs } from './my-tabs';
export const myPanelConfig: PanelConfig = {
storagePrefix: 'myapp-design-token-panel',
consoleNamespace: 'myapp',
modalClassPrefix: 'myapp-design-token-panel-modal',
schemaId: 'myapp-design-tokens/v1',
exportFilenameBase: 'myapp-design-tokens',
tabs: myTabs,
};
See <code>configurePanel</code> reference for the full
list of PanelConfig fields.
3. Match your stylesheet
The panel writes to :root using the names you declared. Your stylesheet
needs to consume them — usually via fallback defaults so the page paints
even before any user override:
/* src/styles/tokens.css */
:root {
--myapp-palette-0: #1e1e2e;
--myapp-palette-1: #f38ba8;
/* …rest of the 16-tuple… */
/* Base roles — written by colorExtras.baseRoles */
--myapp-color-bg: var(--myapp-palette-0);
--myapp-color-fg: var(--myapp-palette-7);
/* Semantic roles — written by the semantic tier */
--myapp-color-primary: var(--myapp-palette-4);
--myapp-color-accent: var(--myapp-palette-5);
--myapp-color-danger: var(--myapp-palette-1);
--myapp-color-success: var(--myapp-palette-2);
--myapp-color-warning: var(--myapp-palette-3);
--myapp-color-muted: var(--myapp-palette-8);
}
💡 Tip
The panel never reads these declarations — it only writes through
document.documentElement.style.setProperty(...). Treat your stylesheet as
the source of the defaults; treat the panel as a layer of inline overrides
on top.
4. Add a second scheme (optional)
Schemes appear in the Color tab “Scheme…” dropdown. Add as many as you
like; their order is the insertion order of colorSchemes.
const defaultLight: ColorScheme = {
background: 0,
foreground: 7,
cursor: 7,
selectionBg: 8,
selectionFg: 0,
palette: [
'#fafafa', '#d20f39', '#40a02b', '#df8e1d',
'#1e66f5', '#8839ef', '#179299', '#4c4f69',
'#9ca0b0', '#d20f39', '#40a02b', '#df8e1d',
'#1e66f5', '#8839ef', '#179299', '#5c5f77',
],
shikiTheme: 'github-light',
};
// then, inside colorExtras:
colorSchemes: {
'Default Dark': defaultDark,
'Default Light': defaultLight,
},
If you also want true light/dark switching driven by the page’s
data-theme attribute, see the
secondary cluster recipe and
the Color cluster reference section on
panelSettings.colorMode.
Related
- Color cluster reference — full type
contract,
ColorClusterExtras, and the internal bridge semantics. - Panel CSS tokens — overriding the panel’s chrome colors (separate from your app’s color cluster).
- Lazy color presets — keep a large preset library out of the SSR config blob.