Status: Proposal (not yet implemented). Recommends Approach E (Hybrid). Audience: CEM maintainers, design-system reviewers, downstream platform teams. Decision status: MVP decisions are resolved; see §12 (Decision log).
CEM ships a generated token manifest from canonical markdown specs in packages/cem-theme/src/lib/tokens/*.md.
The exact count is build output, not design-document truth; dist/lib/tokens/generated-token-coverage.md and
manifest-utils.mjs are authoritative. The CSS pipeline is mature and validated. The next strategic step is
exposing the same tokens to non-CSS consumers:
- Figma designers — canvas designs must use canonical CEM token values, not hand-typed hex codes or guesses. Every color swatch, type scale, spacing step, and corner radius in Figma should be a CEM token.
- Application developers on iOS, Android, Compose, SwiftUI, and other non-web runtimes — native code should consume the same vocabulary as the web. Visual parity across platforms requires a single source of truth.
- Markdown specs remain the single source of truth. Every other format is derived, mirroring how
dist/lib/css/*.cssis derived today. No format may author tokens that don't exist in markdown. - The existing CSS pipeline must keep working. The CSS generators capture CEM-specific features
(
color-mix()recipes,.cem-theme-*overrides,@media (forced-colors: active)fallbacks,@containerqueries,data-cem-{coupling,shape,spacing}attribute selectors) that no off-the-shelf transform reproduces. - Tier discipline propagates. Required and recommended tokens emit by default; optional, adapter-only, and deprecated tokens emit behind explicit flags — same contract as the CSS pipeline.
The repo has design notes for token export, but no implemented Style Dictionary config, DTCG build target, Figma import
files, or native platform outputs. Treat this as the first implementation architecture, not a migration plan. Any
prototype artifacts must either become generated outputs under dist/ or be removed before release.
┌─────────────────┐
│ *.md (truth) │
└────────┬────────┘
│ build:docs (compile-markdown.mjs)
▼
┌─────────────────┐ ┌──────────────────────────┐
│ *.xhtml │────────▶│ CSS pipeline (existing) │──▶ dist/lib/css/*.css
│ (h6 + table) │ └──────────────────────────┘
└────────┬────────┘
│ export-tokens.mjs (NEW — extract + classify)
▼
┌─────────────────┐
│ intermediate │
│ JSON │
└────────┬────────┘
│ export-tokens.mjs (NEW — resolve through generated CSS when needed)
▼
┌─────────────────┐ ┌──────────────────────────┐
│ resolved JSON │────────▶│ DTCG emission │──▶ dist/lib/tokens/cem.tokens.json + figma/
│ (per-theme) │ └──────────┬───────────────┘ + cem.tokens.report.{md,json}
└─────────────────┘ │
▼
┌─────────────────────────────┐
│ Style Dictionary (Phase D) │──▶ dist/lib/token-platforms/{ios,android,js,scss,json}/
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ Native Figma library │──▶ Figma Variables (read-only)
│ (Phase E) │
└─────────────────────────────┘
Principles:
- md → XHTML → DTCG JSON → consumers is the new spine. CSS continues from XHTML directly (existing path unchanged).
- DTCG-compatible JSON is the canonical machine-readable export format. It is based on the
Design Tokens Community Group format used by native Figma
variable import and Style Dictionary, but CEM metadata stays namespaced under
$extensions.cem. - Tier-aware emission matches the CSS pipeline; the same
tiercolumn in source tables drives all outputs. - Token classification. Every token belongs to one of three buckets: cross-platform / web-only / voice-audio (see §5).
| Approach | Source format | Cross-platform fan-out | Figma path | Pros | Cons |
|---|---|---|---|---|---|
| A. Style Dictionary on DTCG JSON | DTCG JSON (derived from XHTML) | Style Dictionary DTCG-aware transforms | Native Figma import reads the same JSON | Mature ecosystem; standards-aligned; one source feeds many targets | Adds Style Dictionary dep; needs custom transforms for color-mix() recipes; multi-dim modes need design |
| B. Per-platform generators | XHTML directly (mirror CSS pipeline) | Hand-rolled cem-ios.html, cem-android.html, cem-figma.html generators |
Custom Figma plugin or static JSON | Stays in established pattern; full control over output shape | Reinvents Style Dictionary's transform library; massive boilerplate per format; per-platform color/font conversion is error-prone |
| C. DTCG JSON only | DTCG JSON | Consumer-owned (apps transform themselves) | DTCG-aware tools consume directly | Lightest weight; pure-standards; future-proof | App teams want native code, not "transform yourself"; high adoption friction |
| D. Native Figma two-way sync | DTCG JSON in repo | Same as A | Figma syncs both ways through a governed connector | Designers can author in Figma, push to repo | Two-way sync risks designers overwriting md source-of-truth; needs strict governance and review gates |
| E. Hybrid (recommended) | XHTML → DTCG JSON → {Style Dictionary, Figma import} | Style Dictionary fans out to iOS/Android/JS | Figma imports or pulls DTCG JSON in a read-only workflow | Best of A + C: web stays as-is, native gets Style Dictionary, Figma consumes standards | Most components to integrate; needs build-time color resolution |
- The existing CSS path already works and captures features Style Dictionary's CSS transform doesn't reproduce
(
color-mix(), theme-mode classes, forced-colors fallbacks, container queries). Replacing it would lose capability. - Approach B duplicates Style Dictionary. Writing per-platform generators by hand means re-implementing color conversion, unit scaling (rem→pt/dp), and platform-idiomatic emission for every target — work the SD ecosystem has already done and battle-tested.
- Approach C punts adoption. App teams asking for tokens want consumable artifacts (Swift constants, Android XML, TS modules), not raw JSON. Friction kills adoption.
- Approach D's two-way sync is a governance landmine. If a designer in Figma renames a token or changes a value, a write-back connector could push it to Git; the canonical md spec is bypassed. Until CEM has a governance process for designer-authored changes, sync stays one-way (read-only from md).
- Approach E preserves all three sources of value: the CSS pipeline keeps producing CSS the way it does today, the DTCG JSON layer becomes the canonical machine-readable export, and Style Dictionary handles platform fan-out without us reinventing it.
Material Design 3 design tokens guide defines a three-tier model:
- Reference tokens — primitives (e.g.,
md.ref.palette.primary40) - System tokens — semantic roles (e.g.,
md.sys.color.primary) - Component tokens — component-specific (e.g.,
md.comp.button.container.color)
CEM's existing layering already maps cleanly:
| M3 tier | CEM equivalent | Examples |
|---|---|---|
| Reference | Branded colors (D0), thickness scale, size scale, dimension scale, breakpoint widths | --cem-color-blue-xl, --cem-typography-thickness-bold, --cem-dim-large |
| System | Emotional palette (D0), action intent tokens, semantic shape/stroke/layering endpoints | --cem-palette-trust, --cem-action-primary-hover-background, --cem-bend-control |
| Component | Controls geometry (D2c), action border-radius, role typography | --cem-control-height, --cem-action-border-radius, --cem-typography-button-font-size |
packages/cem-theme/src/lib/tokens/cem-m3-parity.md already documents the role-by-role mapping. The DTCG export
will (optionally, behind a flag) emit an M3-shaped alias group so consumers expecting M3 names get a drop-in
adapter.
Important framing: M3, Angular Material, MUI, and other external systems are adapter layers, not replacements for CEM tokens. CEM markdown specs remain canonical; CEM CSS custom properties remain the stable web-facing API. Adapter sets (M3-shaped, etc.) are derived outputs, gated behind opt-in flags, and never become the source of truth for any CEM token.
Every CEM token belongs to one of three buckets. This drives what gets exported where:
| Bucket | Examples | DTCG JSON | iOS/Android | SCSS/JS | Figma | Notes |
|---|---|---|---|---|---|---|
| Cross-platform visual | Branded colors, resolved palette/action colors, spacing, typography sizes/weights, timing, shape radii, controls geometry | ✓ | ✓ (resolved) | ✓ | ✓ | Recipes resolve per theme when inputs are portable |
| Web-only | forced-colors fallbacks, @container rules, unresolved system colors, ring composite recipes, CSS selectors and media-query behavior |
— | — | — | — | CSS only (existing pipeline) |
| Voice/audio data | --cem-voice-*-{speech-rate,speech-pitch,speech-volume,ssml-emphasis,ink-thickness,icon-stroke-multiplier} |
✓ (separate file) | v2 / TTS adapters | ✓ | — | Separate cem.voice.tokens.json; not visual |
| Token | Value | Translation strategy |
|---|---|---|
--cem-color-visitedtext-30-black |
color-mix(in srgb, VisitedText 30%, black) |
System color VisitedText resolves only on web/native theme. Other platforms require an explicit mapping or skip report. |
--cem-bend-round |
calc(var(--cem-shape-height, var(--cem-control-height)) / 2) |
Resolve to numeric px/rem at build time; mobile cannot evaluate nested calc() at token load. |
--cem-palette-trust |
light-dark(var(--cem-color-blue-l), var(--cem-color-blue-d)) |
Map to two values per theme: light= and dark=. iOS asset catalogs and Android values-night/ consume them directly. |
--cem-typography-data-font-variant-numeric |
tabular-nums lining-nums |
OpenType feature settings; Android Roboto may not support lining-nums. Document as recommended, not required, on Android. |
--cem-voice-loud-speech-rate |
0.94 |
Not a visual property; routes only to TTS adapter (AVSpeechSynthesizer / Android TextToSpeech). Excluded from visual exports. |
Bucket tagging (above) is per-token. Portability is a finer-grained per-value tag that drives how each token
value is emitted to each target. Every emitted token carries a portability field in $extensions.cem:
| Portability | Meaning | Emission strategy |
|---|---|---|
literal |
Directly portable scalar value | #ecf0ff, 4px, 200ms, 400 — emit as-is to every target |
alias |
References another token; can be expressed as a DTCG reference | DTCG-aware targets get "$value": "{cem.color.blue.l}"; non-aware targets get the resolved primitive |
mode |
Different concrete values per theme mode | Emit as DTCG modes (Figma) or per-resource-qualifier files (Android values-night/, iOS asset catalogs) |
css-expression |
Requires CSS runtime evaluation that cannot be pre-resolved per theme (e.g., currentColor, @container math) |
Skip on non-web targets; document in report; CSS keeps the expression |
platform-note |
Semantically useful but not directly representable on the target (e.g., system colors, SSML emphasis, complex font stacks) | Skip on incompatible targets; emit metadata-only entry to the report |
Rule: the exporter classifies every token at extraction time and propagates the classification through every
output. Reports list every css-expression and platform-note skipped per target so consumers can see what the
target cannot represent — no silent loss.
This taxonomy is orthogonal to the bucket: a "cross-platform visual" token may still be css-expression on
non-web (e.g., --cem-bend-round is calc(...) resolvable per theme; on iOS it becomes literal after
resolution; on a target that only consumes raw DTCG without resolution it stays css-expression).
CEM has multiple orthogonal mode dimensions:
| Mode dimension | Values | Cardinality |
|---|---|---|
| Theme | light, dark, contrast-light, contrast-dark, native |
5 |
| Spacing density | dense, normal, sparse |
3 |
| Coupling | forgiving, balanced, compact |
3 |
| Shape style | sharp, smooth, round |
3 |
Cartesian product = 135 tuples — impractical to express as a single DTCG mode axis.
Strategy:
- Theme is the only exported mode axis. MVP mode values are emitted for
light,dark,contrast-light,contrast-dark, andnative.nativevalues are Chromium-computed browser-reference values for CSS system colors, not guaranteed iOS/Android system equivalents. This maps to:- Figma import mode files, where every imported mode must contain the same token names and types
- iOS asset catalogs (
Any Appearance,Dark, and later high-contrast variants) - Android resource qualifiers (
values/,values-night/, and later high-contrast qualifiers if supported)
- Spacing/coupling/shape are parallel groups. Each appears as its own token group with explicit prefix
(e.g.,
spacing.dense.layout-stack-gap,spacing.normal.layout-stack-gap,spacing.sparse.layout-stack-gap). Consumers select at app level (toggle a CSS attribute on web; pick a Compose theme on Android; etc.). - Forced-colors and reduced-motion are platform-native conventions. Not exported as tokens. iOS adapters
read
UITraitCollection.accessibilityContrast/UIAccessibility.isReduceMotionEnabled; Android readsConfiguration.uiMode/Settings.Global.TRANSITION_ANIMATION_SCALE. Adapter docs (Phase F) describe the mapping per platform.
Counts: 9 parallel groups (3 + 3 + 3) instead of 135 tuples; the theme axis is the only true import/export mode dimension.
Mode-completeness rule. A token must have a concrete value for every mode it claims, or be excluded from all
mode files for that target. Partial mode coverage (token X has a light value but no dark value) is a fail-hard
validation error — Figma collections and iOS asset catalogs both reject mismatched mode sets. Tokens with
incomplete mode coverage are emitted only into the canonical DTCG JSON with a platform-note portability tag and
listed in the per-target report.
CEM's action state tokens use recipes such as
color-mix(in srgb, var(--cem-palette-trust) 70%, var(--cem-palette-trust-x)) for hover state. Two superpowers:
- Theme switching is automatic. If
--cem-palette-trustchanges per theme, hover follows. - Adapter customization. A product adapter can override the base palette and the hover state recipe follows.
Non-web platforms can't evaluate color-mix() at runtime. Two paths:
- Resolve at build time → lose recipe semantics. Each theme produces its own concrete RGBA. Theme switching
becomes per-theme asset bundling (iOS asset catalogs, Android
values-night/). Adapter customization at the recipe level is gone, but adapters can still override at the primitive (palette) level and rebuild. - Export the recipe in a portable form. DTCG composite tokens or string-encoded math, requiring custom consumer logic. Native platforms get nothing useful.
Decision (per portability tier):
literalvalues pass through unchanged.aliasvalues are preserved as DTCG references ("$value": "{cem.color.blue.l}") for consumers that understand them (Figma Variables, Style Dictionary, DTCG-aware tooling). Non-aware consumers get the resolved primitive via Style Dictionary's reference resolver. Aliases preserved over resolution whenever the target supports references — this keeps the semantic graph intact in design tools.modevalues resolve to per-theme primitives at build time and emit per Figma/iOS/Android mode conventions.css-expressionrecipes resolve at build time to per-theme RGBA only if the recipe's inputs are themselves resolvable (e.g.,color-mix(in srgb, var(--cem-palette-trust) 70%, var(--cem-palette-trust-x))— both inputs are alias-resolvable). Otherwise the recipe is reported as CSS-only and skipped on non-web targets.platform-notevalues never resolve; they're skipped with a report entry.
Web keeps recipes verbatim (existing behavior, no change). The CSS pipeline never sees the DTCG JSON layer.
Implementation: packages/cem-theme/scripts/export-tokens.mjs uses Playwright for computed-value capture when a
target needs resolved values. It should serve a minimal fixture over HTTP that loads the generated
dist/lib/css/*.css files, applies .cem-theme-* classes and data-cem-* attributes to <html>, then reads
getComputedStyle(document.documentElement).getPropertyValue('--cem-...') for every token. This follows the repo's
HTTP-served custom-element constraints and lets the browser resolve light-dark(), color-mix(), calc(), and
var() chains from the same CSS output shipped to web consumers.
Do not add a separate color-math dependency for the first implementation. If Playwright/browser capture fails or proves
insufficient, an explicit fallback such as culori may be added to resolve
color-mix(in srgb, ...) per CSS Color Module Level 5 §2.1. That
fallback must be treated as a separate implementation path and validated against browser-computed values.
Add derive*Tokens() companions to derive*Manifest() in
packages/cem-theme/scripts/manifest-utils.mjs. Where the existing manifest functions return
{ name, tier, categoryId }, the new ones return { name, valueRaw, tier, description, mode, category, sourceTable }.
Add packages/cem-theme/scripts/export-tokens.mjs as the orchestration script. Its first stage iterates compiled
dist/lib/tokens/*.xhtml, calls each derivation, builds an in-memory intermediate model, and always writes
dist/lib/tokens/cem.tokens.intermediate.json. This intermediate output is a debug artifact: useful for reviews,
CI artifact inspection, and exporter debugging, but not a public package contract.
Output shape (excerpt):
{
"version": "0.0.7",
"generatedAt": "2026-04-29T10:00:00Z",
"specs": {
"cem-colors": {
"branded": [
{ "name": "--cem-color-blue-xl", "valueRaw": "#ecf0ff", "tier": "required",
"category": "branded", "sourceTable": "cem-color-hue-variant" }
],
"palette": [...],
"action": [...],
"zebra": [...]
},
"cem-shape": {...},
...
}
}cem.tokens.intermediate.json structure:
| Field | Meaning |
|---|---|
$extensions.cem.generated |
Generated provenance: package version, timestamp, command, source specs, and debug marker. |
specs |
Map of source spec names such as cem-colors, cem-shape, and cem-timing. |
specs[*].tokens or grouped arrays |
Extracted token rows before browser resolution. Each token includes source table id and row data. |
token name |
Original CSS custom property name. |
token valueRaw |
Raw value from the source table or derived table formula before CSS/browser resolution. |
token row |
Source table cells retained for debugging column extraction and derivation bugs. |
Consumers must not import cem.tokens.intermediate.json; its shape may change whenever extractor internals change.
The same export-tokens.mjs script resolves values that non-web targets cannot evaluate. Because resolution needs the
generated CSS custom properties, build:tokens depends on build:css, not just build:docs.
For computed capture:
- Launch headless Chromium with Playwright.
- Serve a minimal local HTML fixture over HTTP. The fixture links or injects the generated
dist/lib/css/*.cssfiles. - For each supported theme class (
light,dark,contrast-light,contrast-dark,native):- Set the relevant
.cem-theme-*class on<html>. - Set
data-cem-spacing,data-cem-coupling, anddata-cem-shapeonly for the parallel-group tokens being resolved. - Read
getComputedStyle(document.documentElement).getPropertyValue(token.name). - Capture the resolved RGBA, numeric, dimension, or string value.
- Label
nativevalues as Chromium-computed browser-reference values in metadata and reports.
- Set the relevant
- Keep both
valueRawandvalueByModein memory, and always writedist/lib/tokens/cem.tokens.resolved.jsonas a debug artifact.
Output shape (excerpt):
{
"--cem-palette-trust": {
"valueRaw": "light-dark(var(--cem-color-blue-l), var(--cem-color-blue-d))",
"valueByTheme": {
"light": "rgb(33, 87, 178)",
"dark": "rgb(122, 158, 220)",
"contrast-light": "rgb(0, 0, 102)",
"contrast-dark": "rgb(180, 200, 240)",
"native": "rgb(0, 95, 184)"
}
}
}cem.tokens.resolved.json structure:
| Field | Meaning |
|---|---|
$extensions.cem.generated |
Generated provenance with a debug marker. |
| token key or token record | Original CSS custom property identity. |
valueRaw |
Raw recipe or scalar value before resolution. |
valueByMode |
Browser-computed values for light, dark, contrast-light, contrast-dark, and native. |
resolution |
Metadata for how the value was captured, including theme class, data-cem-* attributes, and Chromium/native note. |
issues |
Per-token resolution warnings such as empty computed value, unsupported system color, or unresolved expression. |
Consumers must not import cem.tokens.resolved.json; use cem.tokens.json, Figma files, or platform outputs instead.
Spacing/coupling/shape mode resolution happens analogously by setting data-cem-spacing, data-cem-coupling,
data-cem-shape attributes on <html> and re-reading the affected tokens. Each becomes a parallel token group in
the DTCG output.
The final stage of export-tokens.mjs transforms the extracted and resolved model to the
DTCG-compatible format.
- Tier-aware filtering (default: required + recommended;
--with-optional,--with-adapter,--with-deprecatedflags add tiers). - Theme mode values in
$extensions.cem.modes, plus separate Figma mode files where needed. - Spacing/coupling/shape as parallel groups.
- Web-only categories filtered out (forced-colors fallbacks, ring recipes, system color references that don't resolve outside web).
- Aliases preserved (
"$value": "{cem.color.blue.l}") when source token references another token; resolution happens only when the consumer can't follow references. - Every emitted token carries
$extensions.cemmetadata pointing back to its markdown source — this is the traceability contract that lets reviewers verify a derived value matches the spec.
$extensions.cem shape (every token):
{
"cem": {
"palette": {
"comfort": {
"$type": "color",
"$value": "{cem.color.blue.l}",
"$description": "Primary comfort surface color.",
"$extensions": {
"cem": {
"cssName": "--cem-palette-comfort",
"spec": "cem-colors",
"sourceTable": "cem-palette-emotion-shift",
"tier": "required",
"category": "d0-palette",
"rawValue": "light-dark(var(--cem-color-blue-l), var(--cem-color-blue-d))",
"portability": "alias",
"modes": {
"light": "rgb(33, 87, 178)",
"dark": "rgb(122, 158, 220)"
}
}
}
}
}
}
}$extensions.cem fields:
| Field | Purpose |
|---|---|
cssName |
Original CSS custom property name (--cem-...) — preserved across all derived outputs |
spec |
Source spec name (e.g., cem-colors) for back-link to markdown |
sourceTable |
h6 id of the source table in the spec |
tier |
required / recommended / optional / adapter / deprecated |
category |
Internal categorization (e.g., d0-palette, d2c-controls) for filtering and grouping |
rawValue |
Original CSS value verbatim (recipe with var()/color-mix()/calc() intact) |
portability |
literal / alias / mode / css-expression / platform-note (see §5.1) |
modes |
Resolved per-theme values when portability is mode or alias with theme variance |
Outputs:
dist/lib/tokens/cem.tokens.intermediate.json— debug-only extraction artifact; not a public contract.dist/lib/tokens/cem.tokens.resolved.json— debug-only browser-resolution artifact; not a public contract.dist/lib/tokens/cem.tokens.json— canonical DTCG JSON, visual tokens (cross-platform bucket).dist/lib/tokens/cem.voice.tokens.json— voice/audio bucket, separate file (for TTS adapters).dist/lib/tokens/cem.tokens.report.md— human-readable report listing every token, its portability, and what was skipped per target and why. First-class output, not optional.dist/lib/tokens/cem.tokens.report.json— machine-readable equivalent for CI assertions.
Generated provenance. Text/code outputs include generated-file headers. JSON outputs cannot use comments, so they
carry provenance in top-level $extensions.cem.generated metadata: package version, generation timestamp, source
spec list, and build command. No generated artifact may ship without provenance.
Add packages/cem-theme/style-dictionary.config.mjs and an Nx target build:token-platforms that depends on
build:tokens. Keep build:css independent so the existing web output can ship even if native exports remain
experimental.
Custom transforms required:
cem/size/layout-to-pt(iOS): spacing, shape, control, and layout dimensions → points.cem/size/type-to-pt(iOS): typography sizes → points; no automatic dynamic type scaling in v1.cem/size/layout-to-dp(Android): spacing, shape, control, and layout dimensions → dp.cem/size/type-to-sp(Android): typography sizes and text line-height → sp.cem/number/unitless: font weights, opacity, z-index/layering, and other numeric values stay unitless.cem/category/web-only-filter: drop web-only categories before emission.cem/mode/expand-themes: expand$extensions.cem.modesto per-platform conventions (asset catalog hints for iOS;values-night/for Android).
Outputs:
dist/lib/token-platforms/
├── ios/
│ ├── CEMTokens.swift (struct CEMTokens { static let colorPaletteTrust = UIColor(...) })
│ ├── CEMTokens.xcassets-hints.json (asset-catalog scaffolding)
│ └── ios-report.md (skipped tokens + reasons)
├── android/
│ ├── values/cem-tokens.xml (light theme)
│ ├── values-night/cem-tokens.xml (dark theme)
│ ├── values-night-hcc/cem-tokens.xml (contrast-dark)
│ ├── compose/CEMTokens.kt (val cemColorPaletteTrust = Color(...))
│ └── android-report.md (skipped tokens + reasons)
├── js/
│ ├── cem-tokens.ts (typed exports + metadata)
│ └── js-report.md
├── json/
│ ├── cem-tokens.json (resolved-per-theme, flat)
│ └── json-report.md
└── scss/
├── cem-tokens.scss ($cem-color-palette-trust: ...;)
└── scss-report.md
Per-platform report contract. Every platform output ships a *-report.md documenting:
- Token coverage: how many of the manifest's required/recommended tokens emitted to this target.
- Skipped tokens with reason:
css-expression unresolvable on iOS,system color VisitedText has no native equivalent on Android,voice token excluded from visual export, etc. - Platform-specific transformation choices applied (e.g.,
layout dimension → dp,typography size → sp,color resolved via Playwright in light theme). - Validation summary: pass/fail counts for each fail-hard rule (see §11).
TypeScript metadata contract. The JS/TS output is for tooling, docs, autocomplete, and tests. It is not a replacement for CSS runtime styling in browsers. At minimum it should expose:
export type CemTokenName = '--cem-palette-comfort' | '--cem-action-primary-default-background';
export interface CemTokenMeta {
name: CemTokenName;
type: 'color' | 'dimension' | 'number' | 'duration' | 'fontFamily' | 'string';
tier: 'required' | 'recommended' | 'optional' | 'adapter' | 'deprecated';
spec: string;
sourceTable: string;
portability: 'literal' | 'alias' | 'mode' | 'css-expression' | 'platform-note';
}The MVP path is native Figma Variables in the CEM UI Kit. All Figma workflows are read-only from the generated artifacts; write-back is deferred until governance is designed.
-
Native Figma library variables (MVP).
- Refresh one
CEM Tokenscollection in the CEM UI Kit from generated token JSON. - Keep five modes in the collection:
Light,Dark,Contrast Light,Contrast Dark, andNative. - Tokens appear as Figma Variables organized by slash-delimited groups.
- Write-back stays disabled until a governance process exists for converting designer edits into markdown spec changes.
- Refresh one
-
Manual Figma Variables import (developer fallback).
- Designers or design-ops drag-drop generated DTCG JSON files into the Variables panel if scripted refresh is not available.
- Lower operational risk than sync; no write-back path.
- Keep this documented as the fallback if file permissions, automation, or mode behavior blocks adoption.
-
Figma REST API sync (post-v1 only).
- Do not include in MVP or default local builds.
- Start as a manual script before considering CI.
- Require explicit file id configuration, scoped write token, dry-run/report mode, generated diff artifacts, and rollback instructions.
- CI integration, if added later, must run only from protected branch/release workflows with required approval.
Figma import has stricter requirements than canonical DTCG JSON. Generate separate Figma files and validate them before native library refresh or direct manual import:
- Import one file per mode (
cem-light.tokens.json,cem-dark.tokens.json,cem-contrast-light.tokens.json,cem-contrast-dark.tokens.json,cem-native.tokens.json). Figma creates or updates variables only when a token is present in every imported mode file and has the same$typein each file. - Emit only supported value shapes: sRGB/HSL colors,
dimensionvalues inpx,durationvalues ins, single-stringfontFamily, numbers, booleans encoded throughcom.figma.type, and strings. - Normalize names exactly as Figma does: nested DTCG groups become slash-separated names. Duplicate normalized names must fail before import because Figma ignores duplicates after the first one.
- Keep aliases inside the same collection where possible. Cross-collection aliases require Figma-specific
com.figma.aliasDatametadata and should be deferred until the one-collection flow is proven. - Exclude unsupported or incomplete-mode tokens from all Figma mode files and list them in
cem-figma-report.md.
examples/figma/README.md will document the native Figma library MVP path plus direct-import and split-collection
prompts.
Use these prompts when intentionally changing the Figma workflow. They preserve the same source-of-truth rule: markdown specs remain canonical, and Figma remains read-only.
Prompt: refresh native Figma Variables
Update the CEM token export Figma workflow to refresh native Figma Variables in the CEM UI Kit.
Keep one CEM Tokens collection. Use the generated files in dist/lib/tokens/figma/ as the only Figma input:
cem-light.tokens.json, cem-dark.tokens.json, cem-contrast-light.tokens.json, cem-contrast-dark.tokens.json,
and cem-native.tokens.json. Preserve read-only governance: Figma changes must become markdown spec edits, not
write-backs. Update docs/todo.md, packages/cem-theme/docs/token-export.md, and examples/figma/README.md.
Prompt: split Figma output by token dimension
Update the CEM token export Figma workflow to split the single CEM collection into dimension-specific collections
only if Figma collection limits or designer navigation justify it. Proposed collections: CEM Color, CEM Dimension,
CEM Typography, CEM Motion, and CEM Platform Notes. Keep markdown specs as source of truth, keep Figma read-only,
and document cross-collection alias handling. If aliases cannot be preserved safely across collections, duplicate
only resolved values and list the loss of alias semantics in cem-figma-report.md. Update docs/todo.md,
packages/cem-theme/docs/token-export.md, and examples/figma/README.md.
Reference applications proving end-to-end consumption:
examples/ios/CEMTokensExample/— minimal SwiftUI app with a button + card using only CEM tokens.examples/android/cem-tokens-example/— minimal Compose app, parallel.examples/web/import-tokens.ts— TypeScript consumption sample with type-safe imports.examples/figma/CEMTokens.fig— sample Figma file (or screenshots) showing token application.
- Update
packages/cem-theme/docs/docs-generation.mdto reference the export pipeline. - Update
packages/cem-theme/src/lib/tokens/index.mdwith a "Platform consumption" section. - Add a "Token export contract" section to
CLAUDE.mdparallel to the existing "Token manifest contract" section. - Cross-reference from
cem-m3-parity.mdto the M3 alias adapter set.
Path layout consolidates exports under dist/lib/tokens/ (canonical + Figma-shaped) and dist/lib/token-platforms/
(Style Dictionary outputs). This mirrors the existing dist/lib/css/ convention and keeps related artifacts
co-located.
packages/cem-theme/
├── docs/
│ └── token-export.md (this file)
├── scripts/
│ ├── manifest-utils.mjs (extended: tokensFromTableWithValues)
│ ├── derive-tokens.mjs (new: full derive*Tokens() helpers)
│ ├── export-tokens.mjs (new: XHTML + CSS → DTCG JSON + Figma + reports)
│ ├── build-token-platforms.mjs (new: Style Dictionary driver)
│ └── validate-platforms.mjs (new: post-emit coverage + name validation)
├── style-dictionary.config.mjs (new)
└── dist/lib/ (generated)
├── tokens/
│ ├── cem-colors.xhtml (existing)
│ ├── ...other spec xhtml... (existing)
│ ├── cem.tokens.json (new: canonical DTCG)
│ ├── cem.voice.tokens.json (new: voice/TTS metadata, v2 adapters)
│ ├── cem.tokens.report.md (new: human-readable portability report)
│ ├── cem.tokens.report.json (new: CI-readable equivalent)
│ ├── cem.tokens.intermediate.json (new: debug-only extraction artifact)
│ ├── cem.tokens.resolved.json (new: debug-only browser-resolution artifact)
│ └── figma/
│ ├── cem-light.tokens.json (Figma mode file)
│ ├── cem-dark.tokens.json (Figma mode file)
│ ├── cem-contrast-light.tokens.json (Figma mode file)
│ ├── cem-contrast-dark.tokens.json (Figma mode file)
│ ├── cem-native.tokens.json (Figma mode file; Chromium native colors)
│ └── cem-figma-report.md (skipped tokens + Figma-specific notes)
└── token-platforms/ (Style Dictionary outputs)
├── ios/
├── android/
├── js/
├── json/
└── scss/
examples/
├── ios/CEMTokensExample/
├── android/cem-tokens-example/
├── figma/README.md
└── web/import-tokens.ts
Rationale for path consolidation:
dist/lib/tokens/already holds the compiled XHTML; adding canonical JSON, reports, and Figma files keeps all token artifacts in one tree.dist/lib/token-platforms/(new) clearly signals "Style Dictionary derived" outputs distinct from CSS or XHTML.- Single
export-tokens.mjsscript handles extraction, classification, CSS-backed resolution, DTCG emission, Figma split, and reports;build-token-platforms.mjsis the Style Dictionary driver. Two scripts, two Nx targets.
The pipeline extends — does not replace — what already exists:
| Existing utility | Path | New use |
|---|---|---|
extractTable(), tokensFromTable() |
packages/cem-theme/scripts/manifest-utils.mjs |
Add tokensFromTableWithValues() capturing value cells, not just names + tier |
| Validator pattern | packages/cem-theme/scripts/validate-manifest.mjs |
Mirror as validate-platforms.mjs for non-CSS outputs |
| Playwright capture/debug scripts | tools/scripts/capture-xpath-text.mjs, debug-cem.mjs |
Model for CSS-backed computed-value capture in export-tokens.mjs |
| Markdown → XHTML compilation | tools/scripts/compile-markdown.mjs |
Unchanged; export pipeline reads its output |
| Nx target wiring | packages/cem-theme/project.json |
Add build:tokens and build:token-platforms targets parallel to build:css |
{
"build:tokens": {
"dependsOn": ["build:css"],
"executor": "nx:run-commands",
"options": {
"command": "node scripts/export-tokens.mjs",
"cwd": "packages/cem-theme"
},
"outputs": [
"{projectRoot}/dist/lib/tokens/cem.tokens.json",
"{projectRoot}/dist/lib/tokens/cem.voice.tokens.json",
"{projectRoot}/dist/lib/tokens/cem.tokens.report.md",
"{projectRoot}/dist/lib/tokens/cem.tokens.report.json",
"{projectRoot}/dist/lib/tokens/cem.tokens.intermediate.json",
"{projectRoot}/dist/lib/tokens/cem.tokens.resolved.json",
"{projectRoot}/dist/lib/tokens/figma"
]
},
"build:token-platforms": {
"dependsOn": ["build:tokens"],
"executor": "nx:run-commands",
"options": {
"command": "node scripts/build-token-platforms.mjs",
"cwd": "packages/cem-theme"
},
"outputs": ["{projectRoot}/dist/lib/token-platforms"]
}
}build:css does not depend on build:tokens — the CSS pipeline ships independently. build:tokens depends on
build:css because resolved non-web values are captured from generated CSS. build can add build:tokens after the
canonical JSON/report outputs are stable, and add build:token-platforms only after native outputs leave the
experimental phase.
The exporter must exit non-zero and fail the build on any of these:
- A token marked
requiredorrecommendedin any spec's manifest is missing fromcem.tokens.json. - Two tokens collide on canonical DTCG path (e.g., both
--cem-foo-barand--cem-foo--barnormalize tocem.foo.bar). - Two tokens collide on Figma slash-normalized name within one collection (e.g.,
--cem-action-primary-default-backgroundand--cem-action-primary-default--backgroundboth normalize toaction/primary/default/background). - Same token name has different
$typeacross mode files (e.g.,colorin light,dimensionin dark). cem.tokens.jsonfails W3C DTCG schema validation.- A platform output references a token that should be filtered out for that platform (e.g., voice token in iOS visual export).
- A generated output is missing the standard generated provenance header or JSON metadata.
- Mode-completeness violation: a token has a value for one mode but not all modes it claims (see §6).
The exporter logs a warning and includes the token in the per-target report (but does not fail):
optional-tier token has no portable value for a target — listed as "skipped optional".css-expressionportability that cannot resolve outside CSS — listed with the original recipe.- System color value (
Highlight,CanvasText, etc.) that has no native equivalent — listed underplatform-note. deprecatedtokens excluded from a target — listed with deprecation note.adapter-tier tokens excluded from default outputs — listed under "opt-in via--with-adapter".
| Phase | Verification |
|---|---|
| A | export-tokens.mjs produces JSON with every manifest-derived token name eligible for export; manifest validator confirms parity with CSS output. |
| B | 5-token spot check per theme matches manual getComputedStyle() capture from tools/scripts/debug-cem.mjs. |
| C | DTCG JSON validates against W3C DTCG schema (@design-tokens/parser or tokens-json-validator). Figma Variables imports without error. Report file lists every skipped token with reason. |
| D | Generated Swift compiles with Xcode 15+; Kotlin with Gradle 8+; TypeScript passes tsc --noEmit; SCSS compiles with dart-sass. Each per-platform report shows zero fail-hard violations. |
| E | Native Figma library workflow loads the generated JSON into one CEM Tokens collection without write-back; direct file import remains documented as a fallback. |
| F | Sample iOS and Android apps render a button + card using only CEM tokens; visual parity with web reference (manual screenshot diff). |
| G | yarn nx affected -t lint test build typecheck green; verify:phase13 green. |
- Change a value in
cem-colors.md(e.g.,--cem-color-blue-xl: #ecf0ff→#e0e8ff). - Run
yarn build. - Verify the new value flows through:
dist/lib/css/cem-colors.css(existing CSS pipeline)dist/lib/tokens/cem.tokens.jsondist/lib/token-platforms/ios/CEMTokens.swiftdist/lib/token-platforms/android/values/cem-tokens.xmldist/lib/token-platforms/js/cem-tokens.ts
- Import or refresh the Figma test collection; verify the new value appears in the Variables panel.
- Diff the per-target report files against the previous build — only the changed token should appear in the diff, with no spurious changes to other tokens' portability or mode classifications.
All decisions needed before MVP extraction/emission are settled. Items marked "deferred" are intentionally post-MVP or require implementation evidence before final commitment.
| # | Decision | Status | Decision | Rationale |
|---|---|---|---|---|
| 1 | Generated output paths | Settled | Use one MVP token artifact tree under dist/lib/tokens/, including canonical JSON, TS metadata, reports, Figma files, and debug artifacts. |
Keeps token artifacts co-located with compiled token XHTML and avoids making canonical JSON look like a platform-specific output. |
| 2 | Build dependency shape | Settled | build:tokens depends on build:css; build:css remains independent. |
Token export resolves non-web values from the exact generated CSS shipped to web consumers. |
| 3 | DTCG validation strategy | Settled | Add a DTCG validator dependency in the MVP path. | External validation gives confidence before Figma and platform exporters consume generated JSON. |
| 4 | Theme mode export | Settled | Export light, dark, contrast-light, contrast-dark, and native; native uses Chromium-computed browser-reference system colors. |
Represents the full CEM theme mode surface while making clear that native colors are browser-reference values, not iOS/Android system equivalents. |
| 5 | Figma MVP workflow | Settled | Native Figma Variables in one CEM Tokens collection; keep direct file import and split-collection prompts for developers. |
Gives designers the preferred workflow while preserving read-only governance and fallback paths. |
| 6 | Debug artifacts | Settled | Always emit cem.tokens.intermediate.json and cem.tokens.resolved.json under dist/lib/tokens/; mark as debug-only/non-contract. |
Useful for CI review and exporter debugging without making these files public package APIs. |
| 7 | Style Dictionary version | Settled | Use latest compatible major at implementation time, pinned by lockfile; prefer v5+ if compatible, otherwise v4+. | Avoids stale doc-pinned versions while still requiring reproducible installs. |
| 8 | Native unit mapping | Settled | Android layout/spacing/shape/control → dp; Android typography → sp; iOS dimensions and typography → points; numeric values stay unitless. | Global px→dp creates accessibility issues; Android typography must scale with system font size while layout dimensions should not. |
| 9 | Color-resolution fallback | Settled | Use browser capture only unless it fails or proves insufficient; add culori only as a validated fallback. |
Browser capture validates against shipped CSS behavior; a JS color library adds a second resolution engine. |
| 10 | Figma REST API sync | Deferred | Defer to post-v1; start with a manual script before CI. Future CI requires explicit file id config, scoped write token, dry-run/report artifacts, and approval. | API sync requires file-id config, write permissions, CI secrets, and rollback planning; it is unnecessary for proving generated artifacts. |
| 11 | Voice token export scope | Deferred | Emit metadata placeholder in v1; defer full TTS adapter support to v2. | No current consumer apps with TTS adapters; metadata-only keeps the taxonomy visible without overbuilding. |
| 12 | Adapter and deprecated tier export | Settled | Gate behind --with-adapter and --with-deprecated. |
Matches the existing CSS pipeline contract. |
| 13 | DTCG mode encoding | Settled | Use canonical $extensions.cem.modes; Figma gets one generated file per mode. |
Multi-mode DTCG support is tool-specific, so CEM keeps mode data namespaced and emits Figma-friendly files. |
| 14 | Output stability commitment | Deferred | Stabilize after Phase D ships once. | Token names in CSS are stable, but DTCG/platform output names need one full release cycle before promising compatibility. |
| 15 | color-mix() resolution policy |
Settled | Resolve at export time per theme; preserve alias-form DTCG for DTCG-aware consumers. | Native platforms get concrete RGBA while DTCG-aware tools retain semantic references. |
| 16 | Native Figma library support tier | Settled | MVP bridge, not source of truth. | The CEM UI Kit is the preferred designer workflow, but markdown remains canonical and direct import remains fallback. |
| 17 | Figma collections | Settled | One CEM collection with groups by dimension; split only if collection limits or navigation friction arise. | One collection simplifies references; split-collection prompts document the future variant. |
| 18 | iOS dynamic type | Settled | Do not auto-scale in v1. | CEM typography sizes are nominal; product apps decide whether to apply dynamic type. |
| 19 | Android color modes | Settled | Resource qualifiers (values-night/) for v1; Compose color schemes optional. |
Resource qualifiers are platform-native and work without app-level Compose wiring. |
| 20 | Package export map for generated JSON/TS | Deferred | Expose generated JSON/TS via package.json exports after generated paths stabilize. |
Consumers should not depend on deep paths, but exports should wait until generated paths are proven. |
| Risk | Impact | Mitigation |
|---|---|---|
Style Dictionary's color-mix() support is incomplete |
Cross-platform colors may emit as raw color-mix(...) strings that don't compile |
Resolve recipes before Style Dictionary sees them; Style Dictionary receives concrete values plus CEM metadata |
| Figma Variables import interprets DTCG modes differently | Designers see different mode behavior than expected | Validate the CEM UI Kit native variables first because it is the MVP path; keep direct file import as fallback and compare mode behavior in the report |
| Multi-mode dimensionality explosion | DTCG output becomes huge and hard to navigate | Theme as the only import/export mode axis; spacing/coupling/shape as parallel groups (9 vs 135) |
| Voice tokens leaking into visual exports | Designers see speech-rate as a "color" or "size" in Figma | Hard-filter via category prefix; separate cem.voice.tokens.json; Style Dictionary's cem/category/web-only-filter excludes voice from visual platform outputs |
| Native platforms can't represent all theme modes | iOS and Android may not have a clean "contrast-light" or "native" equivalent | Canonical JSON/Figma include all theme modes; platform-native exports may map or exclude modes per target report. Native mode is labeled Chromium-reference. |
| DTCG draft format changes before W3C ratification | Output JSON shape may need migration | Pin DTCG schema version in cem.tokens.json $schema; add migration script if the spec revision requires a new wire shape |
- Two-way Figma → md sync. Governance for designer-authored changes is not designed yet. Any GitHub write-back capability stays disabled.
- CI auto-regeneration on token change. Build-on-merge hooks are a future enhancement; v1 ships with manual
yarn build. - Migration of existing component libraries (e.g.,
@epa-wg/cem-components) to consume new exports. Each adapter team owns the migration on their own timeline. - Locale-aware token variants. CEM voice tokens are language-agnostic; per-locale TTS configuration is a product-level concern, not a token-level one.
- Custom Figma plugin for CEM. The native Figma library workflow covers the Figma path. A CEM-specific plugin is an option only if the native workflow proves insufficient — not in v1.
- Dynamic theme synthesis. Themes are statically resolved at build time; runtime theme generation (e.g., from a brand color picker) is a separate feature.
The full Phase A–G arc is the destination. The MVP is the smallest slice that delivers measurable value:
- Canonical DTCG JSON —
dist/lib/tokens/cem.tokens.jsonwith$extensions.cemtraceability and per-theme resolved modes (Phases A + B + C, scoped to required + recommended tiers). - Portability report —
cem.tokens.report.mdlisting every token, its portability classification, and what the canonical export resolved vs. preserved as alias vs. skipped. - Figma file exports — one generated file per theme mode for native Figma library refresh, with direct Figma
Variables file import documented as a fallback. Cover only
literal-portability andalias-portability tokens.nativeuses Chromium-computed browser-reference colors.css-expressionandplatform-notetokens listed incem-figma-report.md. - TypeScript metadata —
cem-tokens.tstyped token names + metadata for consumer tooling, autocomplete in IDEs, and docs generation. Not a runtime CSS replacement. - Defer native — Android XML/Compose and iOS Swift exports stay behind a separate
build:token-platformstarget until unit-mapping policy (decisions §9, §14, §15) and mode coverage are validated.
Why this slice:
- Designers and developers get usable artifacts on day one (DTCG JSON for tooling, Figma files for design).
- The hardest cross-platform questions (px→dp, dynamic type, asset catalogs) are deferred until evidence accrues.
- Native exports are the most expensive to maintain incorrectly — premature shipping creates support load without commensurate adoption.
- Reports surface every gap so the next milestone can prioritize what's missing.
After the MVP ships and stabilizes, native platform exports follow under the experimental stability label.
Generated artifacts have different maturity. The doc commits to explicit stability labels so consumers know what they can rely on:
| Tier | Includes | Stability commitment |
|---|---|---|
| Stable | Markdown specs (packages/cem-theme/src/lib/tokens/*.md), CSS custom properties (dist/lib/css/*.css), required token names |
Backwards compatible; breaking changes only with major version bump and migration notes |
| Beta | Canonical DTCG JSON (cem.tokens.json), TypeScript metadata, portability reports |
Schema may evolve in minor releases; renames discouraged; consumers should pin minor versions |
| Experimental | Figma exports, Android XML/Compose, iOS Swift, asset-catalog hints, direct Figma REST API sync | May change shape, naming, or structure freely between minor releases; consumers regenerate per release |
Breaking changes (require major version bump):
- Removing or renaming a token in the Stable tier.
- Changing a token's
$typein canonical DTCG JSON. - Changing Figma slash-normalized names after a release publishes.
- Changing px→dp / px→pt / px→sp default mapping policy per category.
- Moving a token from
requiredtooptionalin a way that removes it from default outputs. - Removing a portability classification value (
literal/alias/mode/css-expression/platform-note).
Non-breaking changes (minor bump):
- Adding new tokens, specs, or modes.
- Adding new platform outputs (e.g., a new
dist/lib/token-platforms/dart/for Flutter). - Adding fields to
$extensions.cemmetadata. - Tightening portability classification (e.g., a token previously
css-expressionbecomesmodeonce resolution is implemented for it).
Generated provenance carries the version and source-spec timestamp so a downstream consumer can detect when their cached copy diverges from the upstream package version.
- W3C Design Tokens Community Group format
- Material Design 3 — How to use design tokens
- Style Dictionary
culoricolor library- CSS Color Module Level 5 §2.1 (
color-mix()) - Figma Variables modes
- Figma Variables REST API
- CEM internal:
packages/cem-theme/src/lib/tokens/cem-m3-parity.md— M3 role mapping - CEM internal:
packages/cem-theme/docs/docs-generation.md— md → XHTML → CSS pipeline - CEM internal:
packages/cem-theme/docs/token-export-codex.md— superseded rationale merged into this canonical design document - CEM internal:
CLAUDE.md— token manifest contract