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:
- Declare the tab in the host manifest
- Add matching CSS variable declarations in the host stylesheet
- Wire a UI element so it consumes a semantic easing token
- 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-invariable is overridden on:rootlive. -
Because
--myapp-easing-tab-openis declared asvar(--myapp-easing-ease-in), thetransitionon.myapp-tab-panelpicks 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 Openfromease-intoease-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/ 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/ 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.
Related
- Custom token manifest — all value-kind
variants and the full
TabConfigshape. - Apply pipeline setup — full wiring for the bin server, CORS, and the routing JSON.
- Token manifest reference —
TierConfig,TierItem, andreferencesTiercontract.