SUIT CSS Conventions — Dual Architecture

Proof of concept. This page demonstrates SUIT conventions on the dual-architecture POC, which still uses --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 →

§1 — SUIT CSS Naming Overview

SUIT CSS divides styles into utilities (structural traits) and components (self-contained UI units). The naming structure:

TypePatternExample
Utilityu-<camelCase>u-flex, u-textCenter, u-gap-sm
Component<Namespace>-<ComponentName>Iris-Button, Iris-Card
Modifier--camelCaseIris-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

§2 — Utility System

Low-level structural traits. Applied directly to any element. Use !important to ensure override. All values from --dual-* tokens.

Display

u-block u-inlineFlex u-flexAlignCenter u-hidden (not visible)
<div class="u-flex u-gap-sm">
  <span class="u-block">...</span>
  <span class="u-inlineFlex u-flexAlignCenter">...</span>
</div>

Flexbox

Start u-flexJustifyBetween End

Spacing

u-padding-md applied

u-marginT-sm + u-textMuted + u-textSmall

Gap scale (u-gap-*) & owl stacks (u-stack-*)

The token-bound gap scale on a flex row (xs → xl):

u-gap-xs u-gap-sm u-gap-md u-gap-lg

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 -->

Text

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

§3 — Grid System

Flexbox-based responsive grid using SUIT naming. Cells accept fractional widths and responsive modifiers.

Grid-cell--1of3 × 3
1/3
1/3
1/3
Grid-cell--1of4 + Grid-cell--3of4
1/4 (sidebar)
3/4 (content)
<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">

§4 — SUIT Component Naming

Components use PascalCase. The current dual architecture maps to SUIT as:

CurrentSUIT ConventionRole
.Iris-Button.Iris-ButtonComponent root
.Iris-Button--primary.Iris-Button--primaryModifier
.Iris-Button-icon.Iris-Button-iconDescendant (single hyphen)
.Iris-Card-title.Iris-Card-titleDescendant
.Iris-Card-footer.Iris-Card-footerDescendant
disabled attr.Iris-Button.is-disabledState (adjoining)

Live example — utilities + components together

<button class="Iris-Button Iris-Button--primary u-marginR-sm">
  Primary + utility spacing
</button>

§5 — State Classes

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 */ }

§6 — Variables & Web Components (kept as-is)

Two things do not change with SUIT adoption:

VariableResolves fromRole
--dual-button-fillvar(--dual-action-fill-1)Background color
--dual-button-fill-hovervar(--dual-action-fill-1-hover)Hover background
--dual-button-contentvar(--dual-action-content-1)Text/icon color
--dual-button-border-colortransparentBorder color
--dual-button-focusvar(--dual-ui-focus)Focus-ring color
--dual-card-fillvar(--dual-base-fill-2)Card background
--dual-card-title-colorvar(--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;
}

§7 — Custom CSS Compilation Guide

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

Combining with web components

<!-- 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.

§8 — Naming Schemas (the machine-readable contract)

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 fileGovernsKey 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