--dual-* variables and Dual- classes. iris-core will adopt this mechanism additively under the iris namespace (--iris-*, Iris- SUIT classes, <iris-*> tags) — no separate dual namespace ships. See research/README.md.
Live components rendered from the prototype SCSS. Same source compiles to both shadow-DOM (web component)
and light-DOM (CSS classes) via a $namespace parameter.
src/config.yaml)The build is config-driven: build-css.mjs and build-wc.mjs read one src/config.yaml and emit only the enabled outputs. Global knobs — namespace (Dual-), variablePrefix (dual), style, sourcemap, modes, tokenSubset — apply across them. Each layer compiles once to its own granular file under dist/css/; presets then compose consumer load tiers under dist/preset/ from those layers.
cssCompiled entry | Produces (granular) | Notes |
|---|---|---|
variables | dist/css/variables.css | CSS custom properties, 5 mode blocks + per-breakpoint @media — the variable layer alone |
base | dist/css/base.css | reset + base element typography (opinionated globals) |
utilities | dist/css/utilities.css | u-* classes, alone |
grid | dist/css/grid.css | flexbox grid + CSS Grid + bento + 10 layout primitives, alone |
Each layer writes to its own file: — config-gated, so a disabled layer emits nothing and drops out of every preset. Sections that share a file: target are still bundled into that one file. | ||
components (include: false/"all"/[names]) | dist/<file> or dist/<dir>/{name}.css | bundled to file:, or split per-component when dir: is set; false compiles none |
webComponents (bundle/dir/include) | dist/components.js (+ optional dist/<dir>/{name}.js) | shadow-DOM components (build-wc.mjs); bundle:true → one file, dir: → per-component |
presets tier | Produces (barrel) | Composes |
|---|---|---|
variables-only | dist/preset/variables-only.css | the CSS-variable layer alone (~35 KB) |
foundation | dist/preset/foundation.css | variables + base + utilities + grid, no components (~52 KB) |
full | dist/preset/full.css | everything enabled — the everything-bundle these demos load; rule-equivalent to the old bundled base.css |
A preset names an ordered list of the layers above; a layer that is disabled (or fails) is skipped, so the same cssCompiled toggles gate both the granular files and the barrels. | ||
The config surface exists at two points: the iris-core build (what the package emits into dist/) and the consumer (what they load / override). Same declarative model, two owners.
| Stage | Owner | Config surface | Controls |
|---|---|---|---|
| 1 · Token compile | iris-core | tokens/*.config.yaml + compile-tokens-scss.mjs | which Figma sources / reference tiers become generated SCSS |
| 2 · CSS build | iris-core | src/config.yaml → cssCompiled.{…} | namespace, variablePrefix, modes, tokenSubset; which layers emit as granular files |
| 3 · Preset assembly | iris-core | src/config.yaml → presets.{…} | which layers compose each load-tier barrel (variables-only / foundation / full) |
| 4 · WC build | iris-core | cssCompiled.webComponents | which components ship as JS; bundle vs per-component |
| 5 · Load-tier selection | consumer | which dist/preset/*.css or dist/css/*.css entry they import | bytes on the wire — variables-only vs foundation vs full, or hand-picked layers |
| 6 · Theme / override | consumer | semantic + component --dual-* variables (see Consumer Theming) | brand recolor at the semantic layer, per-component exceptions at the component layer |
Stages 1–4 are the iris-core-side build (a future consumer iris.config + CLI, spec'd in P5, will let a consumer drive stages 2–5 declaratively too). Stages 5–6 are what every consumer controls today with zero tooling.
When Stencil builds dual web components, the SCSS compiles with $namespace: '',
producing generic class names (.button, .button--primary)
that are safely encapsulated inside the shadow DOM. The component variables (--dual-button-*)
still inherit through the shadow boundary, allowing consumer overrides.
These use <dual-*> custom elements with shadow DOM — same compiled CSS, generic class names encapsulated.
// packages/core/src/components/button/button.scss
// (Stencil web component — shadow-DOM path)
@use '@siemens-dual/dual-core/src/components/button/styles' as btn;
@use '@siemens-dual/dual-core/src/mixins/shadow-dom/component' as base;
@include base.dual-component();
@include btn.styles($namespace: ''); // → .button (shadow-safe)
:host { display: inline-flex; }
:host([disabled]) { pointer-events: none; }
// Same variables (--dual-button-*) inherit through shadow boundary
// so consumer overrides work identically.
// The compiled shadow-DOM output contains:
.button {
background: var(--dual-button-fill);
color: var(--dual-button-content);
border: 1px solid var(--dual-button-border-color);
}
.button:hover {
background: var(--dual-button-fill-hover);
color: var(--dual-button-content-hover);
}
.button--primary {
--dual-button-fill: var(--dual-action-fill-1);
--dual-button-content: var(--dual-action-content-1);
}
// Component tokens are color-only; dimensions bind semantic vars directly.
// No .dual- prefix — encapsulated in shadow DOM.
| Component | Size | Variables | Selectors |
|---|---|---|---|
| button | 4.8 KB | 114 | 20 |
| card | 1.9 KB | 45 | 8 |
| checkbox | 2.8 KB | 50 | 17 |
| dropdown | 4.9 KB | 96 | 15 |
| radio | 2.6 KB | 47 | 17 |
| text-field | 4.2 KB | 85 | 19 |
| Total | 21.1 KB | 437 | 96 |
When the SCSS compiles with $namespace: 'dual-', the output uses namespaced
class names (.Dual-Button, .Dual-Card) that are
safe to use in global light DOM without collision. No JavaScript runtime needed — pure CSS.
These render using compiled .dual-* classes from _all-components.css — no web component runtime, just CSS.
Component variables can be overridden at any scope — component tokens are color-only, so these buttons have a scoped fill/content override:
Scope: --dual-button-fill: #6d28d9; --dual-button-content: #fff; --dual-button-fill-hover: #5b21b6;
// Your app's SCSS — import dual component styles
@use 'dual/src/components/button/styles' as btn;
@use 'dual/src/components/card/styles' as card;
// Emit with default namespace for light-DOM use
@include btn.styles(); // → .Dual-Button, .Dual-Button--primary, ...
@include card.styles(); // → .Dual-Card, .Dual-Card-title, ...
// Override component color vars globally or per-scope:
:root {
--dual-button-fill: var(--my-brand-primary);
}
.hero {
--dual-button-fill: var(--my-brand-primary);
--dual-button-content: #ffffff;
}
<!-- Load token layer + component CSS -->
<link rel="stylesheet" href="dual/css/components/button.css">
<!-- Use classes directly — no web component needed -->
<button class="Dual-Button Dual-Button--primary">
Save changes
</button>
<button class="Dual-Button Dual-Button--ghost Dual-Button--small">
Cancel
</button>
<!-- Override via scope (component tokens are color-only) -->
<section style="--dual-button-fill: #6d28d9;">
<button class="Dual-Button Dual-Button--primary">Recolored</button>
</section>
| Component | Size | Variables | Selectors |
|---|---|---|---|
| button | 4.9 KB | 114 | 20 |
| card | 1.9 KB | 45 | 8 |
| checkbox | 2.9 KB | 50 | 17 |
| dropdown | 4.9 KB | 96 | 15 |
| radio | 2.6 KB | 47 | 17 |
| text-field | 4.3 KB | 85 | 19 |
| Total | 21.6 KB | 437 | 96 |
Both output paths (shadow-DOM components and light-DOM CSS) inherit these from the single SCSS source compiled into the granular dist/css/*.css layers and the dist/preset/full.css barrel.
type($family,$group,$size,$weight); sans+slab emit per group; block mixins blockquote()/intro()/topline().
--dual-size-content-* / --dual-size-fluid-* + dual-space($n).
theme() emits both .dual-theme-* and [data-dual-theme] — nestable per subtree.
src/config.yaml compiles each layer once to its own file under dist/css/, then a presets: block composes three consumer load tiers under dist/preset/. A layer disabled in cssCompiled emits nothing and drops out of every barrel — so one toggle changes what a tier contains. Tiers below carry their real built sizes (this page loads preset/full.css).
[variables]
The --dual-* CSS-variable layer alone (all 5 modes + per-breakpoint @media). Zero utility/grid/component rules — theme a non-SUIT app off the tokens.
[variables, base, utilities, grid]
Variables + reset/element type + u-* utilities + grid & layout primitives. No components — bring your own, or the shadow-DOM WCs.
[variables, base, utilities, grid, components]
Everything enabled. Under the default config components are off, so full is rule-equivalent to foundation (and to the former bundled base.css). Enable cssCompiled.components and full gains ~18 KB of .Dual-Button/Card/… rules while foundation stays untouched — that is the gate.
| Granular layer | Size | Contains |
|---|---|---|
dist/css/variables.css | 35.1 KB | --dual-* custom properties (5 modes + breakpoints) |
dist/css/base.css | 3.3 KB | reset + base element typography |
dist/css/utilities.css | 6.0 KB | u-* utility rules |
dist/css/grid.css | 7.5 KB | FlexGrid + CssGrid + 10 layout primitives |
See also: SUIT CSS Conventions — utilities, grid, and SUIT naming applied to this architecture.