--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.
Raw values with no usage context. Stored as JSON. Two sources:
| Source | Count | Types | Relationship |
|---|---|---|---|
| iris Figma | 268 | colors, fonts, dimensions, spacing, borders | ↕ Bi-directional (Figma ↔ iris-core) |
| Siemens iX | 28 | status colors (alarm, success, warning, info, critical, neutral) | → One-way copy into iris-core |
// 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"
}
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.
// 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;
}
Toggle Light/Dark above — these boxes read --dual-* semantic tokens:
Color-only, per-component, dynamically created to fit each component's needs. They resolve FROM semantic tokens (never reference directly).
// 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);
/* 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
┌─────────────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────────────┘
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.
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.
<!-- 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>
/* ── 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 */
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.
<!-- 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);
}
/* ── 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);
}
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.
<!-- 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>
/* ── 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 */
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.
<!-- 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); }
/* ── 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);
}
| Method | When | Scope | Shadow DOM | Light 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 |
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.
blockquote() block mixinEvery $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.
[data-dual-theme]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