Zudo Token Panel

Type to search...

to open search from anywhere

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:

  1. colorExtras must be absent. Omitting it prevents resolveColorClusterFromTab from running, which is the only way multiple kind: 'color' tiers can safely coexist in one tab.
  2. One tier = one group. Each TierConfig represents a named colour group. Its label becomes the group heading in the UI.
  3. CSS variable name convention: --palette-{group}-{n}, where {group} matches the tier id and {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/styles/palette.css, 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.

Revision History