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 WCAG contrast overview that evaluates every palette colour as foreground against a selected palette colour as the background.
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
Use the Edit / Check segmented switcher at the top of the Palette tab to open the WCAG contrast checker. The Background (base) column on the left lists every palette colour as a selectable row with its swatch and name; the selected base row is highlighted.
The Foreground vs base column on the right evaluates every palette colour against the selected base. Each row shows a live Aa sample, the WCAG contrast ratio, and a Fail, AA, or AAA verdict chip. The chips use fixed semantic colours so their meaning stays consistent across panel themes.
Use the Flat list toolbar toggle to switch between grouped and flat listings. Enable Large text to use the 3.0 AA threshold instead of the 4.5 threshold for normal text. The footer summarizes the current result as N of M palette colors pass AA as normal/large text on {base}.
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:5173Where 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.