Grouped palette tab
Wire a standalone Palette tab with multiple colour groups, OKLCH editing, and WCAG contrast checking — without colorExtras.
The Palette tab is a purpose-built tab for standalone colour palettes that
are not attached to a colour cluster. Unlike the reserved 'color'/'color-secondary'
tabs (which require colorExtras), a palette tab holds multiple groups of OKLCH
colour tokens and exposes two interaction modes:
- Edit mode — an X-Y curve editor for adjusting Lightness/Chroma/Hue across each group of steps.
- Check mode — a Left/Right WCAG contrast checker to verify that any two palette steps meet accessibility ratio targets.
Use this recipe when you want a free-standing set of named colour steps (e.g. grayscale, brand, accent) that do not feed a semantic colour-scheme table.
Data-model conventions
Before writing the TabConfig, understand the three rules the palette tab
enforces:
colorExtrasmust be absent. Omitting it preventsresolveColorClusterFromTabfrom running, which is the only way multiplekind: 'color'tiers can safely coexist in one tab.- One tier = one group. Each
TierConfigrepresents a named colour group. Itslabelbecomes the group heading in the UI. - CSS variable name convention:
--palette-{group}-{n}, where{group}matches the tieridand{n}is the one-based step number.
Step 1: Declare the palette tab
// src/lib/my-tabs.ts
import type { PanelConfig } from '@takazudo/zdtp';
type TabConfig = PanelConfig['tabs'][number];
export const paletteTab: TabConfig = {
id: 'palette',
label: 'Palette',
// colorExtras intentionally omitted — required for multiple kind:'color' tiers
// to be safe in one tab. The Palette tab relies on this absence.
tiers: [
{
id: 'grayscale',
label: 'Grayscale',
items: [
{
id: 'grayscale-1',
cssVar: '--palette-grayscale-1',
label: 'Grayscale 1',
default: 'oklch(95% 0.005 240)',
type: { kind: 'color', format: 'oklch' },
},
{
id: 'grayscale-2',
cssVar: '--palette-grayscale-2',
label: 'Grayscale 2',
default: 'oklch(80% 0.008 240)',
type: { kind: 'color', format: 'oklch' },
},
{
id: 'grayscale-3',
cssVar: '--palette-grayscale-3',
label: 'Grayscale 3',
default: 'oklch(55% 0.010 240)',
type: { kind: 'color', format: 'oklch' },
},
{
id: 'grayscale-4',
cssVar: '--palette-grayscale-4',
label: 'Grayscale 4',
default: 'oklch(30% 0.012 240)',
type: { kind: 'color', format: 'oklch' },
},
],
},
{
id: 'brand',
label: 'Brand',
items: [
{
id: 'brand-1',
cssVar: '--palette-brand-1',
label: 'Brand 1',
default: 'oklch(90% 0.08 250)',
type: { kind: 'color', format: 'oklch' },
},
{
id: 'brand-2',
cssVar: '--palette-brand-2',
label: 'Brand 2',
default: 'oklch(70% 0.16 250)',
type: { kind: 'color', format: 'oklch' },
},
{
id: 'brand-3',
cssVar: '--palette-brand-3',
label: 'Brand 3',
default: 'oklch(50% 0.20 250)',
type: { kind: 'color', format: 'oklch' },
},
{
id: 'brand-4',
cssVar: '--palette-brand-4',
label: 'Brand 4',
default: 'oklch(30% 0.18 250)',
type: { kind: 'color', format: 'oklch' },
},
],
},
{
id: 'accent',
label: 'Accent',
items: [
{
id: 'accent-1',
cssVar: '--palette-accent-1',
label: 'Accent 1',
default: 'oklch(88% 0.10 55)',
type: { kind: 'color', format: 'oklch' },
},
{
id: 'accent-2',
cssVar: '--palette-accent-2',
label: 'Accent 2',
default: 'oklch(68% 0.18 50)',
type: { kind: 'color', format: 'oklch' },
},
{
id: 'accent-3',
cssVar: '--palette-accent-3',
label: 'Accent 3',
default: 'oklch(48% 0.22 45)',
type: { kind: 'color', format: 'oklch' },
},
{
id: 'accent-4',
cssVar: '--palette-accent-4',
label: 'Accent 4',
default: 'oklch(30% 0.16 42)',
type: { kind: 'color', format: 'oklch' },
},
],
},
],
};
Add paletteTab to PanelConfig.tabs:
// src/lib/my-panel-config.ts
import type { PanelConfig } from '@takazudo/zdtp';
import { paletteTab } 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: [
paletteTab,
// …other tabs…
],
applyEndpoint: 'http://127.0.0.1:24681/apply',
applyRouting: {
// 'palette' matches every --palette-* variable and routes them to
// this file. The bin rewrites the :root block atomically on Apply.
palette: 'src/styles/palette.css',
},
};
📝 Note
The applyRouting key 'palette' matches every CSS variable that starts with
--palette-. If you rename the prefix (e.g. to --myapp-palette-), update the
routing key to match and change all cssVar strings accordingly.
Step 2: Add matching CSS declarations in the host stylesheet
Declare every palette variable inside :root. The apply pipeline rewrites this
block when the user clicks Apply to source files.
/* src/styles/palette.css */
:root {
/* Grayscale */
--palette-grayscale-1: oklch(95% 0.005 240);
--palette-grayscale-2: oklch(80% 0.008 240);
--palette-grayscale-3: oklch(55% 0.010 240);
--palette-grayscale-4: oklch(30% 0.012 240);
/* Brand */
--palette-brand-1: oklch(90% 0.08 250);
--palette-brand-2: oklch(70% 0.16 250);
--palette-brand-3: oklch(50% 0.20 250);
--palette-brand-4: oklch(30% 0.18 250);
/* Accent */
--palette-accent-1: oklch(88% 0.10 55);
--palette-accent-2: oklch(68% 0.18 50);
--palette-accent-3: oklch(48% 0.22 45);
--palette-accent-4: oklch(30% 0.16 42);
}
Step 3: Use palette variables in your CSS
Reference the palette tokens anywhere in your stylesheets. The panel overrides
:root in-memory on every keystroke, so changes appear immediately.
.myapp-hero {
background-color: var(--palette-brand-2);
color: var(--palette-grayscale-1);
}
.myapp-badge {
background-color: var(--palette-accent-2);
}
.myapp-text-muted {
color: var(--palette-grayscale-3);
}
Step 4: Explore the two panel modes
Open the panel (Alt+Shift+P by default, or window.myapp.showDesignPanel())
and navigate to the Palette tab.
Edit mode (default)
The Edit mode shows an X-Y curve editor for each group. Drag the curve handles to adjust the Lightness, Chroma, or Hue ramp across all steps in a group simultaneously. Individual steps can be fine-tuned by clicking them directly in the chart.
All palette variables on :root are updated live as you drag.
Check mode
Click the Check toggle to enter WCAG contrast checking. Two colour swatches appear (Left and Right) — pick any two palette steps from any group and the panel computes the contrast ratio, showing whether the pair meets AA or AAA thresholds.
Use Check mode to validate that your text colours meet contrast requirements against your background colours before committing the palette.
Step 5: Write palette changes back to source
Once you are happy with the palette, click Apply to source files in the
panel. The zdtp-server bin reads applyRouting, matches every --palette-*
variable to src/, and rewrites the :root {} block in that
file with the current panel values.
Start the bin alongside your dev server:
pnpm exec zdtp-server \
--routing ./panel-routing.json \
--write-root ./src/styles \
--allow-origin http://localhost:5173
Where panel-routing.json contains:
{
"palette": "src/styles/palette.css"
}
💡 Tip
Import the routing JSON into your PanelConfig so the UI config and the bin
stay in sync:
import routing from '../../panel-routing.json' assert { type: 'json' };
export const myPanelConfig: PanelConfig = {
// …
applyRouting: routing,
}; CSS variable naming: why --palette-{group}-{n}
The convention exists so applyRouting can route all palette vars with a single
short key. If you add a new group, follow the same pattern:
| Group tier id | Step 1 cssVar | Step 2 cssVar | … |
| --- | --- | --- | --- |
| grayscale | --palette-grayscale-1 | --palette-grayscale-2 | … |
| brand | --palette-brand-1 | --palette-brand-2 | … |
| accent | --palette-accent-1 | --palette-accent-2 | … |
Item ids follow the same {group}-{n} pattern (e.g. grayscale-1) and must
be unique across the entire tab — the persisted override map is keyed by item
id.
Related
- Custom token manifest — all
TabConfig,TierConfig, andTierItemfield variants. - Apply pipeline setup — full wiring for the bin server, CORS, and the routing JSON.
- Token manifest reference —
TierValueKindand thekind: 'color'/format: 'oklch'contract. - Apply pipeline reference — how
applyRoutingmaps variable prefixes to source files.