--dual-* variables and Dual- classes. iris-core will adopt SUIT for CSS classes under the iris namespace (.Iris-Button, .Iris-Button--primary); --iris-* variables and <iris-*> tags keep their current naming. See research/README.md.
Extending the dual-output architecture with SUIT CSS naming conventions, a utility system, and a responsive grid. Reference →
SUIT CSS divides styles into utilities (structural traits) and components (self-contained UI units). The naming structure:
| Type | Pattern | Example |
|---|---|---|
| Utility | u-<camelCase> | u-flex, u-textCenter, u-gap-sm |
| Component | <Namespace>-<ComponentName> | Iris-Button, Iris-Card |
| Modifier | --camelCase | Iris-Button--primary |
| Descendant | -camelCase (single hyphen) | Iris-Card-title, Iris-Button-icon |
| State | .is-stateName (adjoining) | Iris-Button.is-disabled |
| Variable | --ComponentName-cssProperty | --Iris-Button-backgroundColor |
Low-level structural traits. Applied directly to any element. Use !important to ensure override. All values from --dual-* tokens.
<div class="u-flex u-gap-sm">
<span class="u-block">...</span>
<span class="u-inlineFlex u-flexAlignCenter">...</span>
</div>
u-padding-md applied
u-marginT-sm + u-textMuted + u-textSmall
u-gap-*) & owl stacks (u-stack-*)The token-bound gap scale on a flex row (xs → xl):
Owl-selector stacks add top-margin between flow siblings (no flex needed) — u-stack-md:
First paragraph
Second — spaced by --dual-gap-medium
Third — same rhythm
<div class="u-flex u-gap-lg">…</div> <!-- flex gap, token-bound -->
<div class="u-stack-md"><p>…</p><p>…</p></div> <!-- owl stack: * + * margin-block-start -->
u-textCenter u-textBold
u-textTruncate: This is a very long line of text that should be truncated with an ellipsis when it overflows
u-textMuted u-textSmall
Flexbox-based responsive grid using SUIT naming. Cells accept fractional widths and responsive modifiers.
<div class="Grid">
<div class="Grid-cell Grid-cell--1of3">...</div>
<div class="Grid-cell Grid-cell--1of3">...</div>
<div class="Grid-cell Grid-cell--1of3">...</div>
</div>
<!-- Responsive: full on mobile, half on md, third on lg -->
<div class="Grid-cell Grid-cell--full Grid-cell--md-1of2 Grid-cell--lg-1of3">
Components use PascalCase. The current dual architecture maps to SUIT as:
| Current | SUIT Convention | Role |
|---|---|---|
.Iris-Button | .Iris-Button | Component root |
.Iris-Button--primary | .Iris-Button--primary | Modifier |
.Iris-Button-icon | .Iris-Button-icon | Descendant (single hyphen) |
.Iris-Card-title | .Iris-Card-title | Descendant |
.Iris-Card-footer | .Iris-Card-footer | Descendant |
disabled attr | .Iris-Button.is-disabled | State (adjoining) |
<button class="Iris-Button Iris-Button--primary u-marginR-sm">
Primary + utility spacing
</button>
States use is-stateName as an adjoining class. Never styled alone — always scoped to a component:
/* Correct: scoped to the component */
.Iris-Button.is-disabled { opacity: 0.5; pointer-events: none; }
.Iris-Card.is-expanded { max-height: none; }
/* Wrong: never style a state class alone */
.is-disabled { /* ❌ applies to everything */ }
Two things do not change with SUIT adoption:
--dual-<component>-<property>[-state] — already namespaced and working.<dual-button>, <dual-card>, etc. — custom element names must be lowercase with a hyphen (HTML spec), so PascalCase doesn't apply to them.| Variable | Resolves from | Role |
|---|---|---|
--dual-button-fill | var(--dual-action-fill-1) | Background color |
--dual-button-fill-hover | var(--dual-action-fill-1-hover) | Hover background |
--dual-button-content | var(--dual-action-content-1) | Text/icon color |
--dual-button-border-color | transparent | Border color |
--dual-button-focus | var(--dual-ui-focus) | Focus-ring color |
--dual-card-fill | var(--dual-base-fill-2) | Card background |
--dual-card-title-color | var(--dual-text-primary) | Title text color |
Component tokens are color-only. Dimensions, typography, and spacing bind semantic vars (or literals) directly — there is no --dual-button-height or --dual-card-border-radius component token.
/* Override at any scope — both WC and CSS class respond */
.my-section {
--dual-button-fill: hotpink;
--dual-button-content: #fff;
--dual-card-fill: #fafafa;
}
Compile exactly what you need — pick components, add utilities and grid, combine with web components on the same page:
// my-site.scss — custom build from dual architecture
// 1. Token layer (required — resolves all --dual-* variables)
// 2. Pick your components
@use 'dual/src/components/button/styles' as btn;
@use 'dual/src/components/card/styles' as card;
// 3. Add utilities (optional)
@use 'dual/src/utilities';
// 4. Add grid (optional)
@use 'dual/src/grid/grid';
// 5. Emit component styles with your namespace
@include btn.styles($namespace: 'dual-');
@include card.styles($namespace: 'dual-');
// Output: your-site.css contains ONLY button + card + utilities + grid
// Size: a scoped subset instead of the full 51.8 KB preset/full.css bundle
//
// Or use the config-driven build: set components.include = ["button","card"]
// in src/config.yaml and run: node build-css.mjs
<!-- Load token layer -->
<!-- Load only the CSS you compiled -->
<link rel="stylesheet" href="my-site.css">
<!-- Load web components (they inherit --dual-* through shadow boundary) -->
<script src="dual/dist/components.js"></script>
<!-- Mix both on the same page -->
<div class="FlexGrid">
<div class="FlexGrid-cell--1of2">
<!-- Web component: no classes needed -->
<dual-card heading="Dashboard">Content here</dual-card>
</div>
<div class="FlexGrid-cell--1of2">
<!-- CSS class: no JS needed -->
<div class="Iris-Card u-padding-lg">
<h3 class="Iris-Card-title u-textBold">Stats</h3>
<button class="Iris-Button Iris-Button--primary u-sizeFull">View</button>
</div>
</div>
</div>
Key insight: Utilities compose with components in the HTML. The grid is itself a component.
Web components and CSS classes share the same --dual-* token contract, so they can coexist
on the same page — override one variable and both paths respond.
SUIT is the convention; the POC also ships three JSON schema files under schema/ that make each naming contract machine-readable — the single source for how names are formed, so the build and any future generator agree. Each is the authority for one surface:
| Schema file | Governs | Key rules |
|---|---|---|
suit-class-naming-schema.json |
Light-DOM SUIT class names | namespace prefix (default Dual-), component / descendant (-part) / modifier (--variant) / state (is-*) / utility (u-*) forms; records the empty-namespace shadowDomForm the same SCSS emits |
variable-naming-schema.json |
CSS custom-property names at every tier | prefix (--dual-); how reference (primitive), semantic, and per-component color names are formed — the semantic NAME is everything after the prefix |
web-component-naming-schema.json |
Shadow-DOM custom elements | tag prefix (dual-) + class prefix; observed attributes ({{attr}} / {{attr|default}} in <name>.template.html); slots; how the element consumes --dual-* through the shadow boundary |
The three schemas partition cleanly: SUIT classes are the light-DOM contract, variable-naming is the CSS-variable contract, and web-component-naming is the shadow-DOM contract — each cross-references the others. iris-core adopts the same three under its Iris- / --iris- / iris- namespaces.
Back to: Dual-Output Architecture (the full POC with live shadow-DOM + light-DOM components) · SCSS Structure · CSS Overview