Three-Layer Token Architecture

Proof of concept. This page demonstrates the three-tier token model on the dual-architecture POC (still --dual-*). iris-core will adopt the component-token tier additively under the iris namespace, color-only by design (dimensions/typography/spacing bind semantic variables directly). See research/README.md.

Reference → Semantic → Component. Single source of truth with two override points.

§1 — Architecture Overview

iris Figma Library ↕ Bi-directional sync Siemens iX → One-way copy (status only) Layer 1: Reference Tokens (JSON) 296 raw values (268 iris + 28 iX) • colors, fonts, dimensions, spacing, borders • No usage context tokens/reference-tokens.json Layer 2: Semantic Tokens (JSON → SCSS → CSS) 73 tokens per mode • 5 modes (light/dark/sand/wireframe/dark-brand) • Purpose-driven tokens/Color-Theme.<Mode>.json → src/vars/ → CSS vars Form/table tokens excluded (moved to component layer) values from Layer 3: Component Tokens (SCSS → CSS) Color-only • Per-component • Dynamic • In _variables.scss --dual-{component}-{property}[-{state}] defaults from Web Components Shadow DOM inherits vars CSS Classes Light DOM, .dual-* prefix Consumer Override Points A) SCSS variable (pre-compile) B) CSS variable (post-compile) Both work on semantic + component layers Utilities Grid Mixins All consume semantic tokens ↑

§2 — Reference Tokens Layer 1

Raw values with no usage context. Stored as JSON. Two sources:

SourceCountTypesRelationship
iris Figma268colors, fonts, dimensions, spacing, borders↕ Bi-directional (Figma ↔ iris-core)
Siemens iX28status colors (alarm, success, warning, info, critical, neutral)→ One-way copy into iris-core

Reference Token Examples

// From reference-tokens.json
{
  "name": "--dual-ref-colors-petrol-petrol-500",
  "source": "iris",
  "type": "color",
  "value": "#005c6c",
  "note": "formerly Teal 74"
}

{
  "name": "--dual-ref-ix-alarm",
  "source": "ix",
  "type": "color",
  "value": "#ff2453",
  "note": "iX status: theme-color-alarm"
}

Reference Color Swatches (from installed package)

iris brand + petrol (measured from @siemens-iris/iris-core)
iX status (measured from @siemens/ix classic-dark)

§3 — Semantic Tokens Layer 2

Usage-named, mode-aware tokens that resolve from reference. 73 tokens per mode across 5 modes (light, dark, sand, wireframe, dark-brand). The compilation pipeline: JSON → SCSS (per-mode) → main SCSS (scoped to theme classes) → CSS custom properties.

The semantic layer also carries a responsive --iris-layout-* tier (22 layout vars per breakpoint across 5 breakpoints) plus responsive typography composites per breakpoint — dimension, spacing, and typography bind these semantic vars directly rather than minting component tokens.

Semantic Token Examples (mode-aware)

// From Color-Theme.Light.json (one file per theme mode)
{
  "name": "--dual-action-fill-1",
  "type": "color",
  "value": "#00c1b6"          ← resolves from ref: petrol-80-petrol-light
}
// Color-Theme.Dark.json carries the same token with its dark value:
{
  "name": "--dual-action-fill-1",
  "type": "color",
  "value": "#00cccc"          ← resolves from ref: interactive-coral-100-200
}

// Compiled to CSS (in semantic-tokens.css):
:root {
  --dual-action-fill-1: #00c1b6;
  --dual-text-1: #000028;
  --dual-base-1: #f3f3f0;
}
.dual-theme-dark {
  --dual-action-fill-1: #00cccc;
  --dual-text-1: #ffffff;
  --dual-base-1: #00183b;
}

Live Theme Demo

Toggle Light/Dark above — these boxes read --dual-* semantic tokens:

--dual-action-fill-1
--dual-base-2
--dual-base-inverse-1

§4 — Component Tokens Layer 3

Color-only, per-component, dynamically created to fit each component's needs. They resolve FROM semantic tokens (never reference directly).

Dynamic Creation Pattern

// Pattern: --dual-{component}-{property}[-{state}]
// Properties: fill, content, border, shadow, focus
// States: hover, active, disabled, focus, error, selected

// Button component tokens (in _variables.scss):
--dual-button-fill: var(--dual-action-fill-1);
--dual-button-fill-hover: var(--dual-action-fill-1-hover);
--dual-button-content: var(--dual-action-content-1);
--dual-button-content-disabled: var(--dual-action-content-disabled);
--dual-button-focus: var(--dual-ui-focus);

// Card component tokens:
--dual-card-fill: var(--dual-base-1);
--dual-card-border: var(--dual-ui-3);

// Checkbox component tokens:
--dual-checkbox-fill-selected: var(--dual-action-fill-1);
--dual-checkbox-border-hover: var(--dual-ui-interactive-1);

Consumer Override

/* Override via CSS variable (post-compile) — works at runtime */
.my-hero-section {
  --dual-button-fill: hotpink;
  --dual-button-fill-hover: deeppink;
}

/* Override via SCSS variable (pre-compile) — works at build time */
$dual-button-fill: hotpink;
$dual-button-fill-hover: deeppink;
// Then recompile the component SCSS

§5 — Compilation Pipeline

┌─────────────────────────────────────────────────────────────────────────┐
│  reference-tokens.json (296 raw values: 268 iris + 28 iX)                │
│  • iris: colors, fonts, dims, spacing, borders                          │
│  • iX: status colors                                                    │
└───────────────────────────────────┬─────────────────────────────────────┘
                                    │ values consumed by
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  Color-Theme..json (71 tokens each, one file per mode)            │
│  • {name, type, value}  (light / dark / sand / wireframe / dark-brand)  │
└───────────────────────────────────┬─────────────────────────────────────┘
                                    │ compiles to
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  src/vars/_color-light.scss   (128 $variables)              │
│  src/vars/_color-dark.scss    (128 $variables)              │
│  src/vars/_viewport-regular.scss (42 $variables)            │
│  src/vars/_index.scss         (scoped to :root + .dark)     │
└───────────────────────────────────┬─────────────────────────────────────┘
                                    │ compiled by Sass to
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  :root { --dual-action-fill-1: #00c1b6; ... }                          │
│  .dual-theme-dark { --dual-action-fill-1: #00cccc; ... }               │
└───────────────────────────────────┬─────────────────────────────────────┘
                                    │ consumed by
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  Component _variables.scss files                                         │
│  --dual-button-fill: var(--dual-action-fill-1);                         │
│  Compile to CSS vars in both shadow-DOM and light-DOM paths             │
└─────────────────────────────────────────────────────────────────────────┘

§6 — Component Token Usage: Button & Text Input

Complete examples of how component tokens flow from SCSS source → compiled CSS → rendered element, for both output paths (shadow DOM web component + light DOM CSS classes). Each section includes override methods.

Button — Web Component (Shadow DOM)

The <dual-button> web component encapsulates its CSS in a shadow root. The compiled shadow-DOM stylesheet uses generic class names (no prefix) and binds to --dual-button-* component tokens, which inherit through the shadow boundary from the host page's semantic token layer.

▶ Live Rendered — Web Components (Shadow DOM)
Primary Secondary Ghost Disabled
<!-- Usage -->
<dual-button variant="primary">Submit</dual-button>

<!-- What the WC renders in its shadow root: -->
<style>
  .button {
    /* Component tokens declared at the element — values FROM semantic layer */
    --dual-button-fill: transparent;
    --dual-button-fill-hover: var(--dual-action-fill-2-hover);
    --dual-button-content: var(--dual-action-content-2);
    --dual-button-content-disabled: var(--dual-action-content-disabled);
    --dual-button-border-color: var(--dual-action-border-2);
    --dual-button-focus: var(--dual-ui-focus);
    /* ... */

    background: var(--dual-button-fill);
    color: var(--dual-button-content);
    border: 1px solid var(--dual-button-border-color);
    /* ... */
  }
  .button--primary {
    --dual-button-fill: var(--dual-action-fill-1);
    --dual-button-fill-hover: var(--dual-action-fill-1-hover);
    --dual-button-content: var(--dual-action-content-1);
    --dual-button-border-color: transparent;
  }
  .button:hover { background: var(--dual-button-fill-hover); }
  .button:disabled {
    background: var(--dual-button-fill-disabled);
    color: var(--dual-button-content-disabled);
  }
</style>
<button class="button button--primary"><slot></slot></button>

Button WC — Overriding Colors

▶ Live Override Demo — CSS Variable on Host Element
Overridden Primary Custom Brand Normal (no override)
/* ── POST-COMPILE: CSS variable override (works at runtime) ─────────────── */
/* Target the custom element from the light DOM — vars inherit through shadow */

dual-button {
  /* Override ALL buttons on the page */
  --dual-button-fill: hotpink;
  --dual-button-fill-hover: deeppink;
  --dual-button-content: #ffffff;
}

/* Or scope to a section */
.hero-section dual-button {
  --dual-button-fill: var(--dual-base-inverse-1);
  --dual-button-content: var(--dual-text-inverse-1);
}

/* Override at the semantic layer — affects ALL components using that token */
:root {
  --dual-action-fill-1: hotpink;   /* all primary actions turn pink */
}

/* ── PRE-COMPILE: SCSS variable override (requires rebuild) ────────────── */
/* In your consumer-app.scss, before @use-ing the component: */

/* Option A: Override the component token default */
@use '../src/components/button/variables' with (
  $dual-button-fill: hotpink,           /* not available — SCSS @use can't
  $dual-button-fill-hover: deeppink      override vars inside a @mixin */
);

/* Option B (recommended): Override via the semantic layer's SCSS vars */
/* Override the per-mode file BEFORE importing: */
$dual-action-fill-1: hotpink;           /* changes semantic source value */
@use '../src/vars/color-light';  /* picks up your override */

/* Then component tokens automatically resolve to the new semantic value
   because --dual-button-fill: var(--dual-action-fill-1) chain is preserved */

Button — CSS Classes (Light DOM)

The light-DOM output uses .Dual-Button prefixed classes (SUIT CSS naming). Same token architecture, same override mechanics — just class-scoped instead of shadow-scoped.

▶ Live Rendered — CSS Classes (Light DOM)
<!-- Usage -->
<button class="Dual-Button Dual-Button--primary">Submit</button>

.Dual-Button {
  /* Identical component token declarations */
  --dual-button-fill: transparent;
  --dual-button-fill-hover: var(--dual-action-fill-2-hover);
  --dual-button-content: var(--dual-action-content-2);
  --dual-button-content-disabled: var(--dual-action-content-disabled);
  /* ... */

  background: var(--dual-button-fill);
  color: var(--dual-button-content);
  border: 1px solid var(--dual-button-border-color);
}
.Dual-Button--primary {
  --dual-button-fill: var(--dual-action-fill-1);
  --dual-button-fill-hover: var(--dual-action-fill-1-hover);
  --dual-button-content: var(--dual-action-content-1);
}
.Dual-Button:hover { background: var(--dual-button-fill-hover); }
.Dual-Button:disabled {
  background: var(--dual-button-fill-disabled);
  color: var(--dual-button-content-disabled);
}

Button Light DOM — Overriding Colors

▶ Live Override Demo — Scoped CSS Class Override (Light DOM)
/* ── POST-COMPILE: CSS variable override (works at runtime) ─────────────── */
/* Same as WC — target by class instead of element name */

.Dual-Button {
  --dual-button-fill: hotpink;
  --dual-button-content: #ffffff;
}

/* Scoped */
.hero-section .Dual-Button--primary {
  --dual-button-fill: #1a1a2e;
  --dual-button-fill-hover: #16213e;
}

/* ── PRE-COMPILE: SCSS variable override (consumer app, before @use) ───── */
/* In consumer-app.scss: */

/* Option A: Override semantic tokens (affects all consuming components) */
$dual-action-fill-1: #1a1a2e;
$dual-action-fill-1-hover: #16213e;
$dual-action-content-1: #ffffff;

@use '../src/vars/color-light';    /* uses your overrides */
@use '../src/components/button/styles' with ($namespace: 'dual-');  /* compiles */

/* Result: .Dual-Button--primary { --dual-button-fill: var(--dual-action-fill-1); }
   and :root { --dual-action-fill-1: #1a1a2e; }
   → button renders with your custom fill */

/* Option B: Override just this component by wrapping */
.brand-button {
  --dual-button-fill: var(--dual-base-inverse-1);
  --dual-button-content: var(--dual-text-inverse-1);
  --dual-button-fill-hover: var(--dual-base-inverse-2);
}

Text Input — Web Component (Shadow DOM)

The <dual-text-field> web component has more tokens than button (fill, border, content, label, helper, focus, error states). Same inheritance model: semantic tokens flow through the shadow boundary.

▶ Live Rendered — Web Components (Shadow DOM)
<!-- Usage -->
<dual-text-field label="Email" placeholder="you@example.com"></dual-text-field>

<!-- What the WC renders in its shadow root: -->
<style>
  .text-field {
    /* Component tokens — values from semantic layer */
    --dual-text-field-fill: var(--dual-forms-fill);
    --dual-text-field-fill-hover: var(--dual-base-fill-1-hover);
    --dual-text-field-fill-disabled: var(--dual-action-fill-disabled);
    --dual-text-field-border-color: var(--dual-forms-border-1);
    --dual-text-field-border-color-hover: var(--dual-action-fill-1-hover);
    --dual-text-field-border-color-focus: var(--dual-ui-focus);
    --dual-text-field-border-color-error: var(--dual-text-caution);
    --dual-text-field-content: var(--dual-text-primary);
    --dual-text-field-content-disabled: var(--dual-text-disabled);
    --dual-text-field-placeholder: var(--dual-text-secondary);
    --dual-text-field-label-color: var(--dual-text-primary);
    --dual-text-field-helper-color: var(--dual-text-secondary);
    --dual-text-field-error-color: var(--dual-text-caution);
    --dual-text-field-focus-ring-color: var(--dual-ui-focus);
    /* ... */
  }
  .text-field__wrapper {
    background: var(--dual-text-field-fill);
    border: 1px solid var(--dual-text-field-border-color);
    border-radius: 2px;
  }
  .text-field__wrapper:hover {
    background: var(--dual-text-field-fill-hover);
    border-color: var(--dual-text-field-border-color-hover);
  }
  .text-field__wrapper:focus-within {
    border-color: var(--dual-text-field-border-color-focus);
    box-shadow: 0 0 0 2px var(--dual-text-field-focus-ring-color);
  }
  .text-field--error .text-field__wrapper {
    border-color: var(--dual-text-field-border-color-error);
  }
  .text-field__input {
    color: var(--dual-text-field-content);
    font: inherit;
  }
  .text-field__input::placeholder { color: var(--dual-text-field-placeholder); }
  .text-field__label { color: var(--dual-text-field-label-color); }
  .text-field__helper { color: var(--dual-text-field-helper-color); }
  .text-field--error .text-field__helper { color: var(--dual-text-field-error-color); }
</style>
<div class="text-field">
  <label class="text-field__label">Email</label>
  <div class="text-field__wrapper">
    <input class="text-field__input" placeholder="you@example.com">
  </div>
</div>

Text Input WC — Overriding Colors

▶ Live Override Demo — CSS Variable on Host Element (Shadow DOM)
/* ── POST-COMPILE: CSS variable override (works at runtime) ─────────────── */
/* Target the custom element — vars inherit through shadow boundary */

dual-text-field {
  --dual-text-field-fill: #1a1a2e;
  --dual-text-field-content: #e0e0e0;
  --dual-text-field-border-color: #4a4a6a;
  --dual-text-field-border-color-focus: #00d4ff;
  --dual-text-field-placeholder: #888;
}

/* Scope to a dark hero area */
.dark-panel dual-text-field {
  --dual-text-field-fill: rgba(255,255,255,0.05);
  --dual-text-field-border-color: rgba(255,255,255,0.2);
  --dual-text-field-content: #ffffff;
  --dual-text-field-label-color: rgba(255,255,255,0.8);
}

/* Override at the semantic layer — affects ALL form controls */
:root {
  --dual-forms-fill: #f0f7ff;
  --dual-forms-border-1: #2563eb;
}

/* ── PRE-COMPILE: SCSS variable override (requires rebuild) ────────────── */
/* Override semantic token source values before import: */
$dual-forms-fill: #f0f7ff;
$dual-forms-border-1: #2563eb;
$dual-ui-focus: #00d4ff;

@use '../src/vars/color-light';    /* picks up overrides */
/* Component tokens chain to semantic → your values flow through automatically */

Text Input — CSS Classes (Light DOM)

Light-DOM output uses .Dual-TextField prefixed classes. Identical tokens, identical override pattern — just exposed as classes in the document instead of encapsulated in a shadow root.

▶ Live Rendered — CSS Classes (Light DOM)
We'll never share your data
Please enter a valid email
<!-- Usage -->
<div class="Dual-TextField">
  <label class="Dual-TextField-label">Email</label>
  <div class="Dual-TextField-wrapper">
    <input class="Dual-TextField-input" placeholder="you@example.com">
  </div>
  <span class="Dual-TextField-helper">We'll never share your email</span>
</div>

.Dual-TextField {
  --dual-text-field-fill: var(--dual-forms-fill);
  --dual-text-field-fill-hover: var(--dual-base-fill-1-hover);
  --dual-text-field-border-color: var(--dual-forms-border-1);
  --dual-text-field-border-color-hover: var(--dual-action-fill-1-hover);
  --dual-text-field-border-color-focus: var(--dual-ui-focus);
  --dual-text-field-border-color-error: var(--dual-text-caution);
  --dual-text-field-content: var(--dual-text-primary);
  --dual-text-field-placeholder: var(--dual-text-secondary);
  --dual-text-field-label-color: var(--dual-text-primary);
  --dual-text-field-helper-color: var(--dual-text-secondary);
  --dual-text-field-error-color: var(--dual-text-caution);
  --dual-text-field-focus-ring-color: var(--dual-ui-focus);
}
.Dual-TextField-wrapper {
  background: var(--dual-text-field-fill);
  border: 1px solid var(--dual-text-field-border-color);
  border-radius: 2px;
}
.Dual-TextField-wrapper:hover {
  background: var(--dual-text-field-fill-hover);
  border-color: var(--dual-text-field-border-color-hover);
}
.Dual-TextField-wrapper:focus-within {
  border-color: var(--dual-text-field-border-color-focus);
  box-shadow: 0 0 0 2px var(--dual-text-field-focus-ring-color);
}
.Dual-TextField--error .Dual-TextField-wrapper {
  border-color: var(--dual-text-field-border-color-error);
}
.Dual-TextField-input { color: var(--dual-text-field-content); }
.Dual-TextField-input::placeholder { color: var(--dual-text-field-placeholder); }

Text Input Light DOM — Overriding Colors

▶ Live Override Demo — Scoped CSS Class Override (Light DOM)
/* ── POST-COMPILE: CSS variable override (works at runtime) ─────────────── */

.Dual-TextField {
  --dual-text-field-fill: #f0f7ff;
  --dual-text-field-border-color: #2563eb;
  --dual-text-field-border-color-focus: #00d4ff;
}

/* Scoped to a search bar (component tokens are color-only) */
.search-bar .Dual-TextField {
  --dual-text-field-fill: rgba(0,0,0,0.05);
  --dual-text-field-border-color: transparent;
}

/* ── PRE-COMPILE: SCSS variable override (consumer app, before @use) ───── */
/* In consumer-app.scss: */

/* Override semantic tokens that feed the component tokens */
$dual-forms-fill: #f0f7ff;
$dual-forms-border-1: #2563eb;
$dual-ui-focus: #00d4ff;
$dual-text-primary: #1a1a2e;
$dual-text-secondary: #4a4a6a;

@use '../src/vars/color-light';     /* uses overrides */
@use '../src/components/text-field/styles' with ($namespace: 'dual-');

/* Result: text-field tokens default to semantic, which now has YOUR values.
   The full chain:
     $dual-forms-fill (SCSS) →
     --dual-forms-fill (CSS var, compiled from semantic) →
     --dual-text-field-fill: var(--dual-forms-fill) (component token) →
     background: var(--dual-text-field-fill) (element style)
*/

/* Option B: post-compile targeted wrapper */
.brand-input .Dual-TextField {
  --dual-text-field-fill: #1a1a2e;
  --dual-text-field-content: #ffffff;
  --dual-text-field-border-color: rgba(255,255,255,0.3);
  --dual-text-field-placeholder: rgba(255,255,255,0.4);
}

Override Method Summary

MethodWhenScopeShadow DOMLight DOM
CSS var (component)
--dual-button-fill: X
Post-compile (runtime) Single component ✅ Set on dual-button { } ✅ Set on .Dual-Button { }
CSS var (semantic)
--dual-action-fill-1: X
Post-compile (runtime) All components using that semantic token ✅ Set on :root or any ancestor ✅ Same
SCSS var (semantic)
$dual-action-fill-1: X
Pre-compile (rebuild) All components → baked into CSS output ✅ Override before @use ✅ Same
CSS class wrapper
.brand .Dual-Button { --dual-button-fill: X }
Post-compile (runtime) Scoped section of page ✅ Specificity on host ✅ Normal CSS cascade

§7 — New token surfaces (typography · size scales · subtree modes)

Landed in the POC and compiled into the granular dist/css/*.css layers and the dist/preset/full.css barrel: composite typography emitted per family, content-measure + fluid size scales with a dual-space() multiplier, and attribute-scoped theme modes.

Composite typography tokens — per family (sans + slab), parameterized mixin

sans · display · large
slab · title · medium
sans · quote · large — blockquote() block mixin

Every $font-size group (display/title/body/intro/quote/topline) now emits --dual-sans-<group>-* and --dual-slab-<group>-*; the mixin type($family,$group,$size,$weight) composes the style key from these axes.

Size scales — content-measure, fluid, spacing multiplier

--dual-size-content-small (55ch reading measure)
--dual-size-fluid-medium clamp() — resize
dual-space(3) = calc(var(--dual-spacing) × 3)

Attribute-scoped modes — [data-dual-theme]

light
dark
sand

theme() emits both the .dual-theme-* class and the [data-dual-theme] attribute — a subtree switches mode independently. iris keeps its iris-ui-theme-* class names.


Related: Dual-Output Architecture • SUIT CSS Conventions • iris vs iX Comparison