Zudo Token Panel
GitHub repository

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 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:

  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

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/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 idStep 1 cssVarStep 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

CreatedUpdated