Zudo Token Panel

Type to search...

to open search from anywhere

Custom token manifest

Declare custom tabs with TabConfig, TierConfig, and TierItem — including all value-kind variants.

The panel’s tab strip is driven by PanelConfig.tabs, a host-supplied array of TabConfig entries. Each tab owns its tiers; each tier owns its items. There is no global manifest object — tokens live inside the tab that edits them.

This recipe walks through the day-to-day moves: a minimal single-tier tab, every value-kind variant, optional group labels, and advancedTiers disclosure.

For the full type contract see Token manifest reference.

Minimal single-tier tab

The smallest valid tab has one tier and at least one item. Every TierItem carries a discriminated type field instead of flat control/min/max properties — the kind drives which editor the panel renders.

// src/lib/my-tabs.ts
import type { PanelConfig } from '@takazudo/zdtp';

type TabConfig = PanelConfig['tabs'][number];

const spacingTab: TabConfig = {
  id: 'spacing',
  label: 'Spacing',
  tiers: [
    {
      id: 'raw',
      label: 'Spacing',
      items: [
        {
          id: 'myapp-spacing-md',
          cssVar: '--myapp-spacing-md',
          label: 'Medium',
          group: 'core',
          default: '1rem',
          type: { kind: 'length', min: 0, max: 4, step: 0.125, unit: 'rem' },
        },
        {
          id: 'myapp-spacing-lg',
          cssVar: '--myapp-spacing-lg',
          label: 'Large',
          group: 'core',
          default: '1.5rem',
          type: { kind: 'length', min: 0, max: 6, step: 0.125, unit: 'rem' },
        },
      ],
    },
  ],
};

export const myTabs: readonly TabConfig[] = [spacingTab];

Pass the array to PanelConfig.tabs:

// 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,
};

Value-kind variants

TierItem.type is a discriminated union — pick the variant that matches what the CSS variable holds.

length — slider with unit

Use for numeric CSS lengths. The panel renders a slider bounded by min/max with step increments and appends unit to the emitted value.

{
  id: 'myapp-spacing-md',
  cssVar: '--myapp-spacing-md',
  label: 'Medium spacing',
  group: 'core',
  default: '1rem',
  type: { kind: 'length', min: 0, max: 4, step: 0.125, unit: 'rem' },
}

number — unitless slider

Like length but emits a bare number (no unit string appended). Useful for line-height, opacity, font-weight, and similar unitless properties.

{
  id: 'myapp-line-height-body',
  cssVar: '--myapp-line-height-body',
  label: 'Body line height',
  group: 'rhythm',
  default: '1.6',
  type: { kind: 'number', min: 1, max: 2.5, step: 0.05 },
}

select — fixed option list

Renders a dropdown. options is the exhaustive list of allowed values; the panel writes whichever option the user picks.

{
  id: 'myapp-font-family-body',
  cssVar: '--myapp-font-family-body',
  label: 'Body font',
  group: 'family',
  default: 'system-ui',
  type: { kind: 'select', options: ['system-ui', 'Inter, sans-serif', 'Georgia, serif'] },
}

text — free-form CSS string

Renders a plain text input. The value is written to the CSS variable verbatim. Use for anything that does not fit a slider or dropdown: easing functions, cubic-bezier(...) values, composite shorthands, etc.

{
  id: 'myapp-easing-default',
  cssVar: '--myapp-easing-default',
  label: 'Default easing',
  group: 'motion',
  default: 'cubic-bezier(0.4, 0, 0.2, 1)',
  type: { kind: 'text' },
}

color — hex color swatch

Renders a color picker. The value is a CSS hex string (#rrggbb).

{
  id: 'myapp-palette-brand',
  cssVar: '--myapp-palette-brand',
  label: 'Brand',
  group: 'palette',
  default: '#2d6cdf',
  type: { kind: 'color' },
}

Group ids and ordering

group on a TierItem is a plain string — coin whatever ids fit your design system. Groups are just visual section dividers inside a tier; the panel renders a header for each unique group value it encounters.

Items appear in the order they are declared in the items array. If you want groups to cluster together, sort by group before declaring the array. There is no separate groupOrder field on TabConfig — declaration order is the render order.

const items = [
  // core group first
  { id: 'myapp-spacing-sm', ..., group: 'core' },
  { id: 'myapp-spacing-md', ..., group: 'core' },
  // component group after
  { id: 'myapp-spacing-card', ..., group: 'components' },
];

Advanced tiers (disclosure)

Set advancedTiers to an array of tier ids that should render inside the per-tab “Advanced” disclosure rather than in the always-visible list. This is useful for raw scale tiers that expert users might want to tweak but are normally hidden behind the semantic layer.

const fontTab: TabConfig = {
  id: 'font',
  label: 'Font',
  advancedTiers: ['raw'],   // hides the raw scale tier behind disclosure
  tiers: [
    {
      id: 'raw',
      label: 'Type Scale',
      items: [
        { id: 'myapp-scale-sm',  cssVar: '--myapp-scale-sm',  label: 'Scale SM',  group: 'scale', default: '0.875rem', type: { kind: 'length', min: 0.5, max: 2, step: 0.0625, unit: 'rem' } },
        { id: 'myapp-scale-base', cssVar: '--myapp-scale-base', label: 'Scale Base', group: 'scale', default: '1rem',    type: { kind: 'length', min: 0.5, max: 2, step: 0.0625, unit: 'rem' } },
        { id: 'myapp-scale-lg',  cssVar: '--myapp-scale-lg',  label: 'Scale LG',  group: 'scale', default: '1.25rem',  type: { kind: 'length', min: 0.75, max: 3, step: 0.0625, unit: 'rem' } },
      ],
    },
    {
      id: 'semantic',
      label: 'Semantic Roles',
      referencesTier: 'raw',
      items: [
        { id: 'myapp-text-body',    cssVar: '--myapp-text-body',    label: 'Body',    group: 'roles', default: 'myapp-scale-base', type: { kind: 'text' } },
        { id: 'myapp-text-heading', cssVar: '--myapp-text-heading', label: 'Heading', group: 'roles', default: 'myapp-scale-lg',  type: { kind: 'text' } },
      ],
    },
  ],
};

The referencesTier field tells the panel that items in the semantic tier hold ids from the raw tier as their values, not raw CSS values. The apply pipeline emits var(--myapp-scale-base) instead of a literal value when it writes the semantic role to disk. See the Multi-tier tokens recipe for a full worked example.

Read-only display rows

Set readonly: true on items you want to show in the panel without allowing edits. The apply pipeline skips read-only items in both directions.

{
  id: 'myapp-computed-max-width',
  cssVar: '--myapp-computed-max-width',
  label: 'Max width (computed)',
  group: 'layout',
  default: 'min(100%, 64rem)',
  type: { kind: 'text' },
  readonly: true,
}

Pill toggle on a length slider

A pill toggle surfaces a single “special” sentinel value alongside the slider. The user can snap to it with one click and return to a numeric value via a toggle. Common use case: infinite border-radius (9999px).

{
  id: 'myapp-radius-button',
  cssVar: '--myapp-radius-button',
  label: 'Button radius',
  group: 'shape',
  default: '0.375rem',
  type: { kind: 'length', min: 0, max: 1, step: 0.0625, unit: 'rem' },
  pill: { value: '9999px', customDefault: '0.375rem' },
}

pill.value is the sentinel emitted when the user clicks the pill. pill.customDefault is the numeric value restored when they click “Custom”.

Revision History