Zudo Token Panel

Type to search...

to open search from anywhere

Multi-tier tokens

Wire a 2-tier raw + semantic setup with referencesTier so semantic roles resolve via var() to whichever raw token they point at.

A single-tier tab writes a fixed CSS value for every item. A 2-tier setup adds a semantic layer whose items hold pointers to raw-tier item ids rather than raw values. The panel emits var(--raw-cssVar) for each semantic item, so changing the raw value automatically updates every semantic consumer that references it.

This recipe walks through the easing tab from the zfb demo end-to-end:

  1. Declare the tab in the host manifest
  2. Add matching CSS variable declarations in the host stylesheet
  3. Wire a UI element so it consumes a semantic easing token
  4. See the panel update the resolved value live

The concept: reference tier

A TierConfig with referencesTier set does not hold raw CSS values — it holds ids of items from the referenced tier. The apply pipeline reads the pointer and emits var(--referenced-cssVar) in the output file, so the browser resolves the value through the CSS custom-property chain.

raw tier:      --myapp-easing-ease-in = cubic-bezier(0.42, 0, 1, 1)
semantic tier: --myapp-easing-tab-open = var(--myapp-easing-ease-in)

                                       "ease-in" is the raw item id

Changing the raw value — or re-pointing the semantic role to a different raw item — is reflected everywhere the semantic variable is consumed.

Step 1: Declare the easing tab in the host manifest

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

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

export const easingTab: TabConfig = {
  id: 'easing',
  label: 'Easing',
  tiers: [
    {
      // Raw tier: four named easing functions.
      id: 'raw',
      label: 'Raw Easings',
      items: [
        {
          id: 'ease-in',
          cssVar: '--myapp-easing-ease-in',
          label: 'Ease In',
          group: 'easings',
          default: 'cubic-bezier(0.42, 0, 1, 1)',
          type: { kind: 'text' },
        },
        {
          id: 'ease-out',
          cssVar: '--myapp-easing-ease-out',
          label: 'Ease Out',
          group: 'easings',
          default: 'cubic-bezier(0, 0, 0.58, 1)',
          type: { kind: 'text' },
        },
        {
          id: 'ease-inout',
          cssVar: '--myapp-easing-ease-inout',
          label: 'Ease InOut',
          group: 'easings',
          default: 'cubic-bezier(0.42, 0, 0.58, 1)',
          type: { kind: 'text' },
        },
        {
          id: 'linear',
          cssVar: '--myapp-easing-linear',
          label: 'Linear',
          group: 'easings',
          default: 'linear',
          type: { kind: 'text' },
        },
      ],
    },
    {
      // Semantic tier: named roles that point at raw items.
      // referencesTier: 'raw' tells the apply pipeline to emit var(--raw-cssVar).
      id: 'semantic',
      label: 'Semantic Roles',
      referencesTier: 'raw',
      items: [
        {
          id: 'tab-open',
          cssVar: '--myapp-easing-tab-open',
          label: 'Tab Open',
          group: 'roles',
          // Value is the raw item id — not a CSS value.
          default: 'ease-in',
          type: { kind: 'text' },
        },
        {
          id: 'tab-close',
          cssVar: '--myapp-easing-tab-close',
          label: 'Tab Close',
          group: 'roles',
          default: 'ease-out',
          type: { kind: 'text' },
        },
        {
          id: 'modal-enter',
          cssVar: '--myapp-easing-modal',
          label: 'Modal',
          group: 'roles',
          default: 'ease-inout',
          type: { kind: 'text' },
        },
      ],
    },
  ],
};

Add easingTab to your PanelConfig.tabs array:

// src/lib/my-panel-config.ts
import type { PanelConfig } from '@takazudo/zdtp';
import { easingTab } from './my-tabs';
// ...other 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: [
    // ...spacingTab, fontTab, etc...
    easingTab,
  ],
  applyEndpoint: 'http://127.0.0.1:24681/apply',
  applyRouting: { myapp: 'src/styles/tokens.css' },
};

Step 2: Add matching CSS declarations in the host stylesheet

The host stylesheet declares both tiers. The semantic tier uses var() to chain through to the raw tier — that is the exact output the apply pipeline will write on disk when the user clicks Apply.

/* src/styles/tokens.css */
:root {
  /* Easing — raw tier */
  --myapp-easing-ease-in:    cubic-bezier(0.42, 0, 1, 1);
  --myapp-easing-ease-out:   cubic-bezier(0, 0, 0.58, 1);
  --myapp-easing-ease-inout: cubic-bezier(0.42, 0, 0.58, 1);
  --myapp-easing-linear:     linear;

  /* Easing — semantic tier (reference raw via var()) */
  --myapp-easing-tab-open:  var(--myapp-easing-ease-in);
  --myapp-easing-tab-close: var(--myapp-easing-ease-out);
  --myapp-easing-modal:     var(--myapp-easing-ease-inout);
}

📝 Note

The apply pipeline rewrites these exact var names. The prefix myapp in applyRouting is the routing key; the bin matches every --myapp-* variable to this file and updates the block inside :root {} atomically.

Step 3: Consume a semantic easing token in CSS

Wire a UI element to the semantic token. Live edits in the panel update the resolved value without a page refresh because the panel overrides :root in-memory.

/* Any component that should use the configured easing */
.myapp-tab-panel {
  /* Transition uses the semantic easing token.
     When the user changes --myapp-easing-tab-open in the panel, this
     animation updates immediately via the var() chain. */
  transition: transform 0.3s var(--myapp-easing-tab-open);
}

.myapp-modal {
  transition:
    opacity 0.25s var(--myapp-easing-modal),
    transform 0.25s var(--myapp-easing-modal);
}

Step 4: Open the panel and edit a raw value

Open the panel (Alt+Shift+P by default, or call window.myapp.showDesignPanel()). Navigate to the Easing tab.

  • The Raw Easings section shows the four named easing functions as text inputs. Edit Ease In — the --myapp-easing-ease-in variable is overridden on :root live.

  • Because --myapp-easing-tab-open is declared as var(--myapp-easing-ease-in), the transition on .myapp-tab-panel picks up the new value without any further changes.

  • The Semantic Roles section lets you re-point a role at a different raw item. Change Tab Open from ease-in to ease-inout — the panel writes --myapp-easing-tab-open: var(--myapp-easing-ease-inout) on disk when you click Apply.

Step 5: Wire applyRouting so apply-to-disk works

The applyRouting map must cover the new prefix. The bin resolves every --myapp-* variable to src/styles/tokens.css and rewrites the block.

// panel-routing.json
{
  "myapp": "src/styles/tokens.css"
}

Import the same file into your PanelConfig (instead of duplicating the object) so the UI and the bin stay in sync:

import routing from '../../panel-routing.json' assert { type: 'json' };

export const myPanelConfig: PanelConfig = {
  // ...
  applyRouting: routing,
};

Start the bin alongside your dev server:

pnpm exec zdtp-server \
  --routing ./panel-routing.json \
  --write-root ./src/styles \
  --allow-origin http://localhost:5173

Clicking Apply in the panel rewrites src/styles/tokens.css so the var() chains from Step 2 now reflect the current panel state.

Hiding the raw tier behind disclosure

If raw tokens are an implementation detail that most users should not touch, add the raw tier id to advancedTiers. The tier collapses behind an “Advanced” disclosure link; the semantic roles stay visible at the top level.

const easingTab: TabConfig = {
  id: 'easing',
  label: 'Easing',
  advancedTiers: ['raw'],   // collapses raw tier behind disclosure
  tiers: [
    { id: 'raw', label: 'Raw Easings', items: [ /* ... */ ] },
    {
      id: 'semantic',
      label: 'Semantic Roles',
      referencesTier: 'raw',
      items: [ /* ... */ ],
    },
  ],
};

How the apply pipeline handles reference tiers

When the apply pipeline writes a semantic item whose tier has referencesTier set, it looks up the raw item that matches the semantic item’s current value and emits var(--raw-item-cssVar) rather than a literal string. For example:

| Semantic value (panel state) | Written to CSS file | |---|---| | ease-in (default) | var(--myapp-easing-ease-in) | | ease-inout (user changed) | var(--myapp-easing-ease-inout) |

This means the on-disk representation is always a valid CSS var() reference, not a raw easing string — which preserves the semantic relationship even after Apply.

Revision History