Skip to content

Component Library

The formal component reference. Every component is specified as: purpose → anatomy → tokens → states/variants → copy-ready markup → rules. Markup is framework-agnostic HTML + CSS custom properties; port to React/Vue/Svelte by mapping props to the variants listed.

All components assume dist/control-room.css is loaded. All obey the eight laws (references/design-language.md) — the rules here are the laws made concrete per component. Before shipping any component, run it through checklists/component-checklist.md.

Naming. This reference uses bare names (.panel, .btn) for readability, but the shipped classes are cr-prefixed (.cr-panel, .cr-btn, …) and live in styles/components.css — import that after dist/control-room.css and use the cr- classes directly rather than copying the CSS below. The snippets here are the anatomy/spec; styles/components.css is the implementation.


Foundations recap (used by every component)

Section titled “Foundations recap (used by every component)”
/* chassis idiom — the three lines that make something "Control Room" */
border: var(--brd) solid var(--border); /* square, inked */
box-shadow: var(--shadow-off) var(--shadow-off) 0 var(--shadow-col); /* hard */
border-radius: 0; /* always */

Purpose. The one ✕ treatment, shared by every dismissible surface — Alert, Toast, Modal and Drawer. All four render the same markup, so they share one rule rather than four that drift:

<button type="button" class="cr-alert__close" aria-label="Dismiss"></button>
.cr-alert__close, .cr-toast__close, .cr-modal__close, .cr-drawer__close { … }

It lives in the base layer (styles/base.css), not in any one part file — a per-part consumer imports base first, and duplicating the block per component section is exactly the drift being closed.

  • MUST keep color: inherit and border-color: currentColor. That is what makes one treatment correct on every host: on a --panel surface it resolves to --ink; on a flooded toast it resolves to --on-sig / --on-err. A fixed --panel/--border chip cannot — its border measures 1.21:1 against its own fill in dark/extreme/phosphor, and it would punch a hole in the toast’s Law 2 key.
  • MUST pin font-size / line-height rather than inheriting them. .cr-alert sets no font-size while .cr-toast sets --text-sm; the two previously agreed only by accident of their parents computing to the same value.
  • MUST carry an aria-label — the ✕ glyph is not an accessible name.
  • On coarse pointers the control takes the 44px floor on both axes (WCAG 2.5.5) — it is roughly square, so min-height alone would leave an ~18px-wide target.

Purpose. The default container for a group of related data. The system’s workhorse surface.

Anatomy. A composed instrument bay, top to bottom: eyebrow · ghost index (top-right) · heading (h4) · lede · body · footer pinned to the bottom edge. Bordered box with the hard offset shadow throughout. Every part above the body is optional — a bare <CrPanel> is still just a box.

┌─ .cr-panel ────────────────────────┐
│ EYEBROW · UNIT CR-00 ⌐ 01 ⌐ │ eyebrow (real text) + index (ghost)
│ Display Heading │ title
│ ▏ lede / standfirst │ lede, second-key left rule
│ │
│ (children) │
│ │
│ LAW 6 · TEXTURE ON HARDWARE │ footer, pinned
└────────────────────────────────────┘

Parts. root · bleed · index · eyebrow · title · lede · footer.

Tokens. --panel bg · --border + --brd · --shadow-off/--shadow-col · --ink heading · --font-mono + --type-label-tracking · --sig-accent-2 lede rule · --muted eyebrow/footer.

Variants. weight: default (--brd) | major (--brd-heavy). inset: swap bg to --panel-2 for a recessed sub-region. tone: keys the eyebrow to a signal (work/wait/done/err/idle/accent). marks: adds the shipped .cr-mark registration ticks. bleed: a masked edge texture — see below. ambient: adds a shipped .cr-anim-* loop.

<section class="cr-panel cr-panel--major cr-panel--tone-err cr-mark">
<i class="cr-panel__bleed" data-bleed="halftone" aria-hidden="true"></i>
<span class="cr-panel__index" aria-hidden="true">01</span>
<p class="cr-panel__eyebrow">Critique · unit cr-00</p>
<h4 class="cr-panel__title">Sessions</h4>
<p class="cr-panel__lede">14 active, 2 failing.</p>
<!-- rows / content -->
<p class="cr-panel__footer">Law 6 · texture on hardware</p>
</section>
  • MUST use the hard offset shadow; NEVER blur it or round the corners.
  • SHOULD use major weight for a panel that is itself a top-level region.
  • NEVER nest more than one shadow depth visually — stacking hard shadows reads as noise. Use inset (--panel-2, no shadow) for sub-regions.
  • MUST keep index decorative — it is aria-hidden, a stamp on the casing rather than a datum. A readable unit id goes in the eyebrow, which is real text.
  • MUST key tone to an actual machine state (Law 2). A tone that means nothing is decoration wearing a signal’s clothes.
  • NEVER put more than one bled panel on a screen (Law 6). The bleed mask fades the texture out before the readout; it is an accent on the most exceptional surface, not a page-wide wash.

The root is a flex column — that is what pins footer to the bottom edge. Content children are lifted above the bleed layer with z-index: 1.


Purpose. The stark title card — the page’s identity and headline state, in the display register (Law 5). Optionally the host for the drip glitch (Law 3).

Anatomy. --brd-heavy box, --shadow-off-lg shadow · mono eyebrow · display h1 · optional lede in the data register · optional ambient scanline.

<header class="mast">
<p class="eyebrow">DP Control Room · Phase 0</p>
<h1>14 sessions<br>2 need you</h1>
</header>
.mast {
border: var(--brd-heavy) solid var(--border);
box-shadow: var(--shadow-off-lg) var(--shadow-off-lg) 0 var(--shadow-col);
background: var(--panel); padding: 22px 24px; position: relative; overflow: hidden;
}
.eyebrow {
font-family: var(--font-mono); font-size: 11px; font-weight: 800;
text-transform: uppercase; letter-spacing: .14em; color: var(--sig-work); margin: 0 0 8px;
}
.mast h1 {
font-weight: 900; font-size: clamp(28px, 5.5vw, 52px); line-height: .9;
letter-spacing: -.038em; text-transform: uppercase; margin: 0; text-wrap: balance;
}
  • MUST keep h1 in the display register only.
  • SHOULD set the eyebrow in --sig-work (or --stage when calm).
  • SHOULD add .cr-mark to the primary readout for the signature registration ticks (industrial crop marks at opposite corners — structure, not a signal; see design-language.md#signatures). Ink weight only; never on a signal-keyed surface where the ticks would read as state.
  • NEVER put body prose in the masthead — that is a mid-register violation.

Purpose. The one region that keys to the state needing attention (Law 2). If nothing needs attention it keys to --stage and stays calm.

Anatomy. Signal-filled box · display “big” line + mono sub-line · optional seeded cat (references/seeded-cat.md).

Variants. state: keys the fill to --sig-{state} (or --stage).

<div class="hero" data-state="wait">
<div>
<div class="big">nova needs you</div>
<div class="sub2">CR-1130 file picker · paused for input · 6m</div>
</div>
<span class="heropet"><!-- cat canvas --></span>
</div>
.hero {
display: flex; align-items: center; gap: 16px;
background: var(--sig-accent); color: var(--on-sig);
border: var(--brd) solid var(--border);
box-shadow: var(--shadow-off) var(--shadow-off) 0 var(--shadow-col);
padding: 16px; position: relative; overflow: hidden;
}
.hero[data-state="wait"] { background: var(--sig-wait); }
.hero[data-state="err"] { background: var(--sig-err); }
.hero[data-state="calm"] { background: var(--stage); color: var(--stage-ink); }
.hero .big { font-weight: 900; font-size: 19px; line-height: 1.05; }
.hero .sub2 { font-family: var(--font-mono); font-size: 12px; opacity: .82; margin-top: 3px; }
  • MUST bind the fill to real state; NEVER pick the hero color for looks.
  • NEVER run two keyed heroes on one screen.

Purpose. Primary navigation. Persistent chassis on the left.

Anatomy. --rail background · --brd-heavy right border · brand block · nav list, active item keyed to --sig-accent · optional count badge.

<nav class="rail">
<div class="brand">CONTROL<br><span class="r">ROOM</span></div>
<ul class="nav">
<li><a class="active" href="#">◈ Attention <span class="badge">1</span></a></li>
<li><a href="#">◧ Sessions</a></li>
</ul>
</nav>
.rail { width: 168px; flex-shrink: 0; background: var(--rail);
border-right: var(--brd-heavy) solid var(--border); padding: 15px 0; }
.brand { font-family: var(--font-mono); font-weight: 900; font-size: 13px;
padding: 0 15px 16px; line-height: 1.15; color: var(--rail-ink); }
.brand .r { color: var(--sig-work); }
.nav { list-style: none; margin: 0; padding: 0; }
.nav a { display: flex; align-items: center; gap: 9px; padding: 9px 15px;
color: var(--rail-ink); opacity: .72; text-decoration: none; font-size: 13px;
font-weight: 700; border-left: 3px solid transparent; cursor: pointer; }
.nav a:hover { opacity: 1; }
.nav a.active { opacity: 1; background: var(--sig-accent); color: var(--on-sig);
border-left-color: var(--border); }
.nav .badge { margin-left: auto; font-family: var(--font-mono); font-size: 10px;
font-weight: 800; background: var(--sig-err); color: var(--on-sig);
padding: 1px 6px; border: 1.5px solid var(--border); }
  • MUST key the active item to --sig-accent; MUST key the count badge to the state it counts (--sig-err for attention items).
  • MUST mark the current item aria-current="page".

Purpose. One row of a live list — the densest unit in the system. Calm at rest; reacts only to a real event (Law 7).

Anatomy. Seeded cat (26px, static) · mono name · StatusDot · mono status.

States. event: a one-shot glitch when this row’s state changes.

<div class="srow">
<canvas class="rowcat" width="26" height="26" aria-hidden="true"></canvas>
<span class="nm">CR-1130 file picker</span>
<span class="dot" style="background: var(--sig-wait)"></span>
<span class="st">needs input</span>
</div>
.srow { display: flex; align-items: center; gap: 11px; padding: 7px 0;
border-bottom: 1.5px solid color-mix(in srgb, var(--border) 18%, transparent); }
.srow:last-child { border-bottom: none; }
.srow .nm { flex: 1; font-family: var(--font-mono); font-size: 12px;
font-weight: 600; color: var(--ink); }
.srow .st { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.srow.event { animation: rowglitch .4s steps(3) 1; }
@keyframes rowglitch {
0%,100% { transform: translateX(0); filter: none; }
33% { transform: translateX(-2px); filter: hue-rotate(20deg); }
66% { transform: translateX(2px); }
}
  • MUST keep rows calm at rest — only trigger .event on a genuine state change, then remove it.
  • NEVER glitch the numerals or the status text (Law 3); the glitch is on the row transform, not the data.

Purpose. The smallest state readout. A square (not a circle — Law: radius 0).

<span class="dot" style="background: var(--sig-work)"
role="img" aria-label="working"></span>
.dot { width: 8px; height: 8px; border: 1.5px solid var(--border);
flex-shrink: 0; display: inline-block; }
  • MUST set the fill from the signal ramp and give it an aria-label naming the state — color is never the only channel.
  • NEVER round it.

Purpose. A compact tag/label keyed to a signal (Law 2). signal takes the canonical vocabulary — work · wait · done · err · idle · accent — and defaults to done, so the bare chip needs no modifier.

States. stamp: a one-shot stamp-in when added.

<span class="chip">PTL-757</span>
<span class="chip work">ui-kit</span>
.chip { font-family: var(--font-mono); font-size: 11px; font-weight: 700;
padding: 3px 9px; background: var(--sig-done); color: var(--on-sig);
border: var(--brd) solid var(--border); }
.chip.work { background: var(--sig-work); }
.chip.stamp { animation: stamp .18s ease-out 1; }
@keyframes stamp { 0% { transform: scale(1.18); opacity: .4; } 100% { transform: scale(1); opacity: 1; } }
  • SHOULD reserve the chip color for a real category, not decoration.

Renamed in 1.0. Chip used to take tone?: "done" | "alt". That was the signal channel under a second prop name — alt resolved to --sig-work — so it is now signal, with alt becoming work. See Law 2 in references/design-language.md for the canonical vocabulary.


Purpose. Primary action. Mechanical snap-press (Law 7, tier 0).

Variants. primary (--sig-wait fill, --brd-heavy) · the secondary “controls” button (--panel fill, --brd) for utilities.

<button class="btn" type="button">RUN SCAN</button>
.btn { font-family: var(--font-mono); font-size: 12px; font-weight: 800;
letter-spacing: .03em; padding: 9px 15px; cursor: pointer;
background: var(--sig-wait); color: var(--on-sig);
border: var(--brd-heavy) solid var(--border);
box-shadow: var(--shadow-off) var(--shadow-off) 0 var(--shadow-col);
transition: transform .05s, box-shadow .05s; position: relative; }
.btn:active { transform: translate(var(--shadow-off), var(--shadow-off));
box-shadow: 0 0 0 var(--shadow-col); } /* press INTO the shadow */
.controls button { font-family: var(--font-mono); font-size: 11px; font-weight: 800;
text-transform: uppercase; letter-spacing: .04em; padding: 7px 11px;
background: var(--panel); color: var(--muted);
border: 2px solid var(--border); box-shadow: 3px 3px 0 var(--border);
cursor: pointer; transition: transform .05s, box-shadow .05s; }
.controls button:active { transform: translate(3px, 3px); box-shadow: 0 0 0 var(--border); }
  • MUST implement the snap-press: on :active the element translates by the shadow offset and the shadow collapses to 0 — it visibly presses into its own shadow. This is the system’s signature interaction.
  • SHOULD limit one primary button per region.
  • NEVER add a hover elevation, a gradient, or a rounded corner.

Purpose. The only legal host for texture (Law 6). A physical instrument enclosing a recessed, textured screen.

Anatomy. --brd-brush casing · corner rivets · inset screen carrying --halftone.

<div class="bezel">
<div class="rivets" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
<div class="screen">
<div class="l">&gt; scan complete · 14 sessions · 2 flagged</div>
</div>
</div>
.bezel { border: var(--brd-brush) solid var(--border); background: var(--panel-2);
padding: 11px; box-shadow: var(--shadow-off) var(--shadow-off) 0 var(--shadow-col); }
.bezel .rivets { display: flex; justify-content: space-between; margin-bottom: 8px; }
.bezel .rivets i { width: 7px; height: 7px; background: var(--border); display: block; }
.bezel .screen { background: var(--board); border: var(--brd) solid var(--border);
padding: 16px; background-image: var(--halftone);
background-size: var(--halftone-size) var(--halftone-size); }
.bezel .screen .l { font-family: var(--font-mono); font-size: 12px; color: var(--ink); }
  • MUST confine --halftone (and scanlines/grain) to .screen.
  • NEVER nest bezels or place more than one instrument per screen.

Purpose. Dense tabular data. Same chassis as a panel.

table { width: 100%; border-collapse: collapse; font-size: 13px;
border: var(--brd) solid var(--border);
box-shadow: var(--shadow-off) var(--shadow-off) 0 var(--shadow-col);
background: var(--panel); }
th, td { text-align: left; padding: 10px 12px;
border-bottom: 1.5px solid color-mix(in srgb, var(--border) 15%, transparent); }
th { font-family: var(--font-mono); font-size: 11px; text-transform: uppercase;
letter-spacing: .06em; color: var(--muted); font-weight: 800; }
td b { color: var(--ink); } td .mono { font-family: var(--font-mono); font-size: 12px; }
  • MUST set headers in the mono/label style; body cells in the data register.

Operator options (the CrTable component). sortable makes each header a button (aria-sort + a .cr-table__ind arrow); selectable adds a leading checkbox column and toggles tr[aria-selected] (the row washes to --sig-work); sticky (.cr-table--sticky) pins the header — wrap the table in an overflow:auto box for it to bite. Rows hover-highlight via --state-hover-mix.

<table class="cr-table cr-table--sticky">
<thead><tr>
<th class="cr-table__sel" aria-label="select"></th>
<th class="cr-table__sortable" aria-sort="ascending">Job<span class="cr-table__ind"></span></th>
</tr></thead>
<tbody>
<tr aria-selected="true"><td class="cr-table__sel"><input type="checkbox" class="cr-check" checked /></td></tr>
</tbody>
</table>
  • MUST keep sort/selection reflected in ARIA (aria-sort, aria-selected) — the visual state is never the only channel.

Purpose. A dense, virtualized table for large datasets — thousands of rows that scroll smoothly because only the rows in (or near) the viewport are in the DOM. Use Table for small, static tabular data; reach for CrDataGrid when the row count is large or unbounded.

<CrDataGrid
columns={[
{ key: "id", label: "ID", sortable: true, align: "end", width: "70px" },
{ key: "host", label: "Host", sortable: true, width: "1fr" },
{ key: "cpu", label: "CPU %", sortable: true, align: "end", width: "90px" },
]}
rows={rows} /* any[]; 10k rows is fine */
rowKey="id" /* stable selection key (default: index) */
selectable /* leading checkbox column + select-all */
height={320} /* scroll viewport px */
rowHeight={34} /* fixed row height — the virtualization basis */
onSortChange={(key, dir) => {}}
onSelectionChange={(keys) => {}}
/>
  • Virtualization: a sizer preserves the full scroll height and the visible window is offset with translateY. rowHeight is a fixed number (fast O(1) path) or a (row, index) => number function for variable-height rows — the offsets come from a prefix-sum and the window from a binary search. Heights must be deterministic from the row.
  • Sorting cycles a sortable header asc → desc → none (stable; copies the array, never mutates the caller’s). Emits onSortChange.
  • Selection is a checkbox column with select-all; keys come from rowKey. Emits onSelectionChange(keys).
  • It’s a div-grid (role="grid" with row/columnheader/gridcell, aria-sort, aria-rowcount, aria-selected), not a <table>, so the virtualization offset composes cleanly across all targets.
  • Keyboard: the grid is a single tab stop and uses the WAI-ARIA active-descendant pattern — arrow keys / Home / End / PageUp / PageDown move an active cell (tracked in state, surfaced via aria-activedescendant, ringed with .cr-grid__cell--active), scrolling it into view even when it was virtualized out. Header sort buttons and row checkboxes are also natively Tab-focusable.

Purpose. Switch between sibling views. A role=tablist of buttons with a keyed underline on the active tab (scalar active-index state in CrTabs).

<div class="cr-tabs" role="tablist">
<button role="tab" class="cr-tab cr-tab--on" aria-selected="true">queue</button>
<button role="tab" class="cr-tab" aria-selected="false">workers</button>
</div>
.cr-tab--on { color: var(--ink); border-bottom-color: var(--sig-work); }
  • MUST set aria-selected on each tab; the underline colour is the ramp (--sig-work), not decoration.

Purpose. Inline status label inside dense content (distinct from Chip, which is a standalone token). Variants map to the signal ramp.

Tone vocabulary. Prefer the canonical ramp words — the same vocabulary a StatusDot, Toast, or Chip asserts (Law 2): done · work · wait · err · idle · accent. The older tell-time aliases (now→done, later→wait, no→err) are kept so nothing breaks, but new markup should use the canonical names.

<span class="cr-tag cr-tag--done">Phase 0</span>
.cr-tag { font-family: var(--font-mono); font-size: var(--text-2xs); font-weight: 800;
padding: var(--space-0-5) var(--space-2); border: 1.5px solid var(--border);
text-transform: uppercase; letter-spacing: .04em; }
.cr-tag--done { background: var(--sig-done); color: var(--on-sig); }
.cr-tag--work { background: var(--sig-work); color: var(--on-sig); }
.cr-tag--wait { background: var(--sig-wait); color: var(--on-sig); }
.cr-tag--err { background: var(--sig-err); color: var(--on-err); }
/* legacy aliases (retained): .cr-tag--now .cr-tag--later .cr-tag--no */

Four shapes, four fixed meanings. Max 15° off-axis. NEVER decorative.

PrimitiveMeaningMechanism
.chevdirectionleft-facing CSS triangle before the label
.notchstateclip-path corner cut
.wedgeactive-panel focusaccent clip-path wedge on the trailing edge
.arrowrail spansequence / pipeline stepchevron-clipped, overlapping steps; .on = current
.chev { position: relative; padding-left: 25px; }
.chev::before { content: ""; position: absolute; left: 8px; top: 50%;
transform: translateY(-50%); width: 0; height: 0;
border-left: 9px solid var(--sig-work);
border-top: 6px solid transparent; border-bottom: 6px solid transparent; }
.notch { background: var(--sig-wait); color: var(--on-sig);
clip-path: polygon(0 0, 100% 0, 100% calc(100% - 11px), calc(100% - 11px) 100%, 0 100%); }
.arrowrail { display: flex; }
.arrowrail span { font-family: var(--font-mono); font-size: 11px; font-weight: 700;
padding: 7px 15px 7px 21px; background: var(--panel); color: var(--ink);
border: var(--brd) solid var(--border); margin-left: -10px;
clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 50%, calc(100% - 10px) 100%, 0 100%, 10px 50%); }
.arrowrail span:first-child { margin-left: 0; }
.arrowrail span.on { background: var(--sig-work); color: var(--on-sig); }
  • MUST pick the primitive by meaning, not by looks.
  • NEVER exceed 15° off-axis on large shapes; the grid must still govern.

Purpose. A grid of state-keyed tiles that reads as one instrument, not competing stages. Each tile keys to its item’s state.

.tiles { display: grid; gap: 0;
grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
border: var(--brd-brush) solid var(--border); }
.tile { aspect-ratio: 1; position: relative; display: flex; align-items: flex-end;
padding: 7px; overflow: hidden;
border-right: var(--brd) solid var(--border);
border-bottom: var(--brd) solid var(--border); }
  • MUST keep tiles uniform in size and gridded — that is what makes many keys read as one sheet rather than clutter.

Purpose. The house glitch for failure. Vertical downward bleed in --drip over a --sig-err field. Reserved for error surfaces and the masthead.

The glitch is signal corruption on a CRT, not liquid running down a wall: vertical scanlines in --drip, masked to fade downward. It is a ::before overlay, so __title and __sub carry their own stacking context.

<div class="cr-drip">
<div class="cr-drip__title">connection lost</div>
<div class="cr-drip__sub">ai-global-chat · SSE closed · retry 3/5</div>
</div>
  • MUST reserve drip for real errors; NEVER decorate a healthy surface with it.
  • MUST draw it in --drip — not in the surface’s own --sig-err, or the glitch stops reading as corruption and becomes a gradient.

Law 3 maps corruption depth to severity, so pick the tier that matches the state rather than dialling a decorative amount:

ClassTierMeaning
.cr-glitch-t1splitnominal
.cr-glitch-t2slicedegraded
(authored content)cursedfailed — zalgo, max 2 combining marks per glyph

.cr-glitch-t2 reads its text from data-glitch:

<span class="cr-glitch-t2" data-glitch="DEGRADED" aria-label="degraded">DEGRADED</span>
  • MUST let the clean string own the accessible name and mark the corrupted copies aria-hidden — a screen reader must never hear the glitch.
  • NEVER glitch data, numerals, labels, or anything under 18px.

Purpose. The zero-data and failure fallbacks for any panel.

  • EmptyState — data register, --muted, a short mono line and one action. No glitch (nothing is wrong), keyed to --stage/calm if colored at all.

  • ErrorState — the drip surface above, or a panel keyed to --sig-err, with the failure named in display and the detail in data.

  • MUST distinguish “nothing here yet” (calm) from “something failed” (error keying + drip). They are different states and must not look alike.


The identity+state sprite that appears in the hero, session rows, and the state strip. It is large enough to warrant its own spec — see references/seeded-cat.md for the deterministic generator, the paint() contract, and the per-state poses.

  • MUST derive fur/markings from the session id (identity) and pose from state — never store a per-session asset.
  • MUST provide a text equivalent (aria-label) naming the session and state; the canvas is decorative to a screen reader.

The whole vocabulary in one screen, using only the shipped cr- classes plus the two lines of page-level layout glue listed below the markup. It exercises the nine laws together: the condensed masthead with registration ticks (.cr-mark), a keyed Hero, the severity shapes beside colour, a seeded Sigil per session, the arrow-rail, a texture + scanline bezel with the ambient scan loop, keyed tiles, and exactly one Law-9 breach. It survives a theme flip with zero per-theme code — see it live, and toggle dark / light / extreme / phosphor, in the Live Gallery (linked at the top of the sidebar).

<div class="cr-instrument">
<nav class="cr-nav" aria-label="Primary">
<div class="cr-nav__brand">CONTROL<br>ROOM</div>
<ul class="cr-nav__list">
<li><a class="cr-nav__item cr-nav__item--active" href="#" aria-current="page">◈ Attention <span class="cr-nav__badge">2</span></a></li>
<li><a class="cr-nav__item" href="#">◧ Sessions</a></li>
<li><a class="cr-nav__item" href="#">▦ Sprint</a></li>
</ul>
</nav>
<div class="cr-instrument__board">
<!-- condensed masthead + the one registration mark -->
<header class="cr-masthead cr-mark">
<p class="cr-masthead__eyebrow">DP Control Room · Phase 0</p>
<h1 class="cr-masthead__title">14 sessions<br>2 need you</h1>
</header>
<!-- the single keyed focal region -->
<div class="cr-hero cr-hero--wait">
<div>
<div class="cr-hero__big">nova needs you</div>
<div class="cr-hero__sub">CR-1130 · paused for input · 6m</div>
</div>
<!-- <CrSigil seed="nova-01" state="waiting" /> -->
</div>
<div class="cr-cols">
<!-- sessions: severity SHAPE (non-colour) + seeded sigil + status -->
<section class="cr-panel cr-panel--major">
<h4 class="cr-panel__title">Sessions</h4>
<div class="cr-row"><span class="cr-sev cr-sev--work" role="img" aria-label="working"></span><span class="cr-row__name">PTL-757 chat-turn</span><span class="cr-row__status">streaming</span></div>
<div class="cr-row"><span class="cr-sev cr-sev--warn" role="img" aria-label="attend"></span><span class="cr-row__name">CR-1130 picker</span><span class="cr-row__status">needs input</span></div>
<div class="cr-row"><span class="cr-sev cr-sev--crit" role="img" aria-label="critical"></span><span class="cr-row__name">rp verify</span><span class="cr-row__status">2 failing</span></div>
<div class="cr-row"><span class="cr-sev cr-sev--ok" role="img" aria-label="nominal"></span><span class="cr-row__name">atlas deploy</span><span class="cr-row__status">merged</span></div>
</section>
<!-- pipeline: arrow-rail + hardware bezel (texture + scan loop) + chrome -->
<section class="cr-panel">
<h4 class="cr-panel__title">Pipeline</h4>
<div class="cr-rail">
<span class="cr-rail__step cr-rail__step--on">scan</span>
<span class="cr-rail__step">triage</span>
<span class="cr-rail__step">fix</span>
<span class="cr-rail__step">verify</span>
</div>
<div class="cr-bezel cr-anim-scan">
<div class="cr-bezel__rivets" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
<div class="cr-bezel__screen">&gt; scan complete · 14 sessions · 2 flagged</div>
</div>
<div class="cr-hw-row"><span class="cr-plate">UNIT · CR-00 · REV.C</span><span class="cr-tally">▐▐▐ ▌ 14</span></div>
</section>
</div>
<!-- the ONE breach: the exceptional item, softened + glowing (Law 9) -->
<div class="cr-breach cr-breach--wash cr-breach--alive">
<p class="cr-masthead__eyebrow">Milestone</p>
<div class="cr-hero__big">Sprint 41 shipped</div>
<div class="cr-hero__sub">38 tasks · 0 regressions · 2 days early</div>
</div>
<!-- keyed contact sheet -->
<div class="cr-tiles">
<div class="cr-tile cr-tile--work">nova</div>
<div class="cr-tile cr-tile--wait">atlas</div>
<div class="cr-tile cr-tile--done">echo</div>
<div class="cr-tile cr-tile--err">rhea</div>
<div class="cr-tile cr-tile--idle">kite</div>
<div class="cr-tile cr-tile--stage">calm</div>
</div>
</div>
</div>

The only page-level CSS is layout glue (the token layer + styles/components.css carry everything else):

.cr-cols { display: grid; grid-template-columns: 1.25fr 1fr; gap: var(--space-3); }
.cr-hw-row { display: flex; gap: var(--space-3); align-items: center; margin-top: var(--space-3); }
/* .cr-breach carries its own panel background + padding; add only your own spacing */
  • MUST keep to one keyed hero and one breach per screen (Laws 2 + 9).
  • MUST pair each row’s colour with its severity shape so state survives the monochrome phosphor theme (Law 4 / accessibility).
  • Everything above is theme-independent — the same markup renders in all four themes with zero overrides.

Neobrutalist inputs on the recessed board surface — square, inked, mono, with the system focus ring. Every control needs an associated label; error state is shown with a non-color marker as well as the --sig-err border (never color alone).

Field wrapper — label + control + hint/error.

<div class="cr-field">
<label class="cr-field__label" for="name">Session name</label>
<input id="name" class="cr-input" placeholder="nova-01" />
<span class="cr-field__hint">lowercase, no spaces</span>
</div>
<div class="cr-field cr-field--error">
<label class="cr-field__label" for="ep">Endpoint</label>
<input id="ep" class="cr-input" aria-invalid="true" />
<span class="cr-field__error">must be a valid URL</span>
</div>

Input / textarea / select.cr-input, .cr-textarea, .cr-select.

CrInput takes two optional in-field affordances. icon="search" paints a glyph on the leading edge; clearable adds a real <button type="button" aria-label="Clear"> on the trailing edge, shown only while the field has a value, firing onClear (and clearing through onChange). Either one wraps the input in .cr-input-wrap and pads that edge; with neither, the component still renders a bare <input>. Both edges use logical properties, so they mirror under RTL.

<span class="cr-input-wrap">
<span class="cr-input__icon" aria-hidden="true"><!-- 16px icon --></span>
<input class="cr-input" data-icon="true" data-clearable="true" value="nova" />
<button type="button" class="cr-input__clear" aria-label="Clear"></button>
</span>

.cr-textarea resizes on both axes and is capped at max-width: 100% so the drag handle cannot pull it outside its container.

A native <select>’s open option list is drawn by the OS, not the page, so it takes none of our border, shadow, font, or padding. We set option background/colour — honoured by Chromium and Firefox on Windows/Linux, ignored on macOS and iOS. A fully styled popup would require abandoning the native element (and with it mobile pickers, type-ahead, and screen-reader semantics); use CrCombobox when that is genuinely needed. Checkbox / radio — square (radius 0); checked fills --sig-work with an --on-sig mark:

<label class="cr-check"><input type="checkbox" checked /> Auto-scan</label>
<label class="cr-check"><input type="radio" name="hue" checked /> Cyan</label>

Switch — a real button[role="switch"] so it’s keyboard-operable and named:

<button type="button" role="switch" aria-checked="true" class="cr-switch">
<span class="cr-switch__track" aria-hidden="true"></span> Live
</button>
  • MUST give every control a label (<label for> or a wrapping <label>).
  • MUST signal error with the marker + border, not color alone.
  • NEVER round a control; disabled uses opacity, not a new color.

cr-instrument is the dashboard chassis: a --brd-brush frame with a hard shadow, composing the Nav rail and a board (masthead/hero + panels).

<div class="cr-instrument">
<nav class="cr-nav" aria-label="Primary">
<div class="cr-nav__brand">CONTROL<br>ROOM</div>
<ul class="cr-nav__list">
<li><a class="cr-nav__item cr-nav__item--active" href="#">◈ Attention <span class="cr-nav__badge">2</span></a></li>
<li><a class="cr-nav__item" href="#">◧ Sessions</a></li>
</ul>
</nav>
<div class="cr-instrument__board">
<div class="cr-hero cr-hero--wait"></div>
<section class="cr-panel"><h4 class="cr-panel__title">Sessions</h4></section>
</div>
</div>
  • MUST keep one keyed focal region (Law 2) in the board — one Hero, not many.
  • SHOULD let the board scroll; the rail stays fixed-width (--cr-nav-w).

Three surfaces that sit above the board. All three are square, inked, hard-shadowed like every other surface — an overlay is not a soft floating card, it’s another panel that happens to stack on top. They obey the same laws (radius 0, hard offset shadow, texture only on hardware) and survive every theme flip.

A blocking dialog built on the native <dialog> element, so the browser owns the focus-trap, Escape-to-close, and the ::backdrop scrim — behaviour that is notoriously easy to get wrong is delegated to the platform and is identical in every framework target. The Mitosis component (CrModal) drives showModal()/close() imperatively from a single open prop and reports the native close event back through onClose.

<dialog class="cr-modal">
<div class="cr-modal__head">
<h2 class="cr-modal__title">Kill session?</h2>
<button type="button" class="cr-modal__close" aria-label="Close"></button>
</div>
<div class="cr-modal__body">CR-1130 is streaming. Terminating drops the turn.</div>
</dialog>
<CrModal open={open} title="Kill session?" onClose={() => setOpen(false)}>
CR-1130 is streaming. Terminating drops the turn.
</CrModal>
  • MUST open with showModal() (not the open attribute) so the backdrop and focus-trap engage; the component does this for you.
  • MUST name the dialog — title becomes aria-label; the is labelled Close.
  • The ::backdrop is --mass at 72% — the black is the scrim (Law 1), never a blur.
  • NEVER round the frame or nest a second modal; one blocking surface at a time.

A transient status readout keyed to a machine signal (Law 2) — the fill is the signal colour, so a toast asserts the same state vocabulary as a StatusDot or a Hero. Errors announce assertively; everything else is polite.

<div class="cr-toast cr-toast--done" role="status">
<span class="cr-toast__msg">3 sessions cleared</span>
<button type="button" class="cr-toast__close" aria-label="Dismiss"></button>
</div>
<div class="cr-toast cr-toast--err" role="alert">
<span class="cr-toast__msg">Endpoint unreachable</span>
</div>
<CrToast signal="err" message="Endpoint unreachable" duration={6000} onClose={dismiss} />
  • MUST map the fill to a real signal (work/wait/done/err) — a toast is state, not chrome. err uses --on-err text; the rest use --on-sig.
  • MUST use role="alert" + aria-live="assertive" for err, role="status"
    • polite otherwise — the component picks this from signal.
  • SHOULD auto-dismiss non-critical toasts (duration); keep errors sticky so they can’t be missed.

Purpose. A fixed screen anchor that stacks live toasts. The parent owns the list (CrToastRegion is presentational); each row stays its own live region so nothing double-announces. Bottom anchors stack newest nearest the edge.

Nine anchors. position takes four corners (tr default · br · tl · bl), the two horizontal centers (tc · bc), and the three vertical middles (ml · mr · mc). Centred anchors use 50% + a translate rather than left:0;right:0, so the region keeps its shrink-to-fit width.

Packing. Consecutive toasts sharing the same message and signal collapse into one row with a ×N counter, so a retry storm costs one row instead of ten. Only consecutive runs pack — an unrelated toast in between keeps the occurrences separate and preserves arrival order.

A packed row carries two ids, and keeping them apart is what makes the row safe inside a live region:

fieldwhich toastwhy
idoldest (first) memberidentity. Stable while the run grows, so the row is patched rather than remounted on a count bump.
newestIdnewest memberdismiss target. onDismiss receives this, so it removes the toast the user is looking at.

Collapsing them into one field is an accessibility bug: an identity that changes on every duplicate remounts the row, and a remounted role="alert" re-announces.

<CrToastRegion position="bc" toasts={list} onDismiss={remove} />
.cr-toast-region { position: fixed; display: flex; flex-direction: column; gap: var(--space-2); }
.cr-toast-region--br { bottom: var(--space-4); right: var(--space-4); flex-direction: column-reverse; }
.cr-toast-region--mc { top: 50%; left: 50%; transform: translate(-50%, -50%); align-items: center; }
  • MUST keep each row’s own role/aria-live (don’t wrap the region in a second live region — that double-announces).
  • MUST keep the ×N counter aria-hidden. It is the only thing that changes when a duplicate arrives; leaving it inside the announced text would re-fire the live region on every repeat, and err announces assertively. (aria-atomic defaults to false, so an AT announces only the changed node — and that node is hidden.)
  • MUST NOT key the row on newestId, in any target. It changes on every duplicate, so it would remount the row and refire the live region. Guarded by tests/cross-fw-contract.test.mjs across all six framework outputs.
  • SHOULD cap how many stack at once and drop oldest, so a burst can’t bury the screen.

Purpose. A dropdown of actions. A trigger toggles a role=menu panel; a transparent full-viewport scrim closes it on outside click — no global listeners, so every framework target behaves the same.

Positioning. Collision-aware, same algorithm as Popover: on open the panel anchors to the trigger via placement (${side} or ${side}-${align}, default "bottom-start"), flips to the opposite side when there’s no room, and shifts along the cross axis to stay in the viewport, tagging itself with data-placement.

<CrMenu label="actions ▾" placement="bottom-end" onSelect={run}
items={[{ label: "pause all" }, { label: "kill all", danger: true }]} />
.cr-menu__panel { position: absolute; z-index: calc(var(--z-overlay) + 1);
box-shadow: var(--shadow-off-sm) var(--shadow-off-sm) 0 var(--shadow-col); }
.cr-menu__item--danger { color: var(--sig-err); }
  • MUST set aria-haspopup="menu" + aria-expanded on the trigger and role="menuitem" on each item.
  • SHOULD reserve --danger for destructive actions only (Law 2).

Purpose. Move through pages of a table/list. Controlled: it renders from page/total and emits onChange; the parent owns the page. Prev/next plus a windowed run of numbers with ellipses; the current page is keyed and aria-current="page".

<CrPagination page={page} total={9} onChange={setPage} />
.cr-pager__btn--on { background: var(--sig-work); color: var(--on-sig); }
.cr-pager__btn[disabled] { opacity: var(--state-disabled-op); }
  • MUST disable prev at page 1 and next at the last page, and mark the current page with aria-current.
  • SHOULD keep the number window small (first · current±1 · last) so the control stays one line at any page count.

Purpose. A keycap badge announcing a keyboard shortcut. It is decorative (aria-hidden) — the real binding rides aria-keyshortcuts on the action. Two registers, matching how prominent the action is:

  • Main actions show the keycap always (<CrKbd keys="I" />).
  • Secondary / bulk actions use the hint variant — hidden until the user hovers/focuses a .cr-keys-host, or peeks all by holding a key (default Alt) via the headless CrKeyHints behavior.
<CrButton keyshortcuts="i" onClick$={openIncident}>open incident <CrKbd keys="I" on /></CrButton>
<div class="cr-keys-host">
<CrButton keyshortcuts="1">dark <CrKbd keys="1" hint /></CrButton>
</div>
<CrKeyHints /> {/* hold Alt to reveal every hint badge at once */}
.cr-kbd--hint { opacity: 0; }
.cr-keys-host:hover .cr-kbd--hint,
:root[data-cr-keys="on"] .cr-kbd--hint { opacity: 1; } /* no layout shift */

Declaring bindings. Pass hints to render a legend. The keys string uses the notation readers already know from editors and docs — + joins a chord (pressed together), a space joins a sequence (pressed in order):

<CrKeyHints
hints={[
{ keys: "Ctrl+K", label: "Open the command palette" }, /* chord */
{ keys: "g p", label: "Go to the sprint board" }, /* sequence */
{ keys: "Ctrl+K p", label: "Palette, then pin" }, /* combined */
]}
/>

The two joins are drawn differently, because that distinction is the whole point of the syntax: chord members sit tight around a + glyph, sequence steps are pushed apart by the word then. Parsing is forgiving: any whitespace run splits a sequence ("g p" == "g\tp" == "g p"), a dangling joiner is dropped ("Ctrl+"Ctrl, "+K"K), and a literal plus key is recovered at any position in a chord ("Ctrl++K"Ctrl + K; a bare "+" is the plus key).

  • MUST pair the badge with aria-keyshortcuts on the actual control — the keycap is a visual, not the accessible name.
  • MUST keep the keycaps decorative. In the legend every keycap and both separators are aria-hidden; each binding instead carries an aria-label of the spoken form and its description (“Control plus K, then P: Palette, then pin”), so a screen reader hears the meaning and not a run of unlabelled boxes. The label sits per binding, not on the list, so the bindings stay separately navigable.
  • MUST NOT put a sequence in aria-keyshortcuts. WAI-ARIA defines that value as a space-separated list of alternative combinations, each pressed simultaneously — so "g p" there asserts “g or p”, the opposite of what this syntax means. The legend therefore emits aria-keyshortcuts only for single-step bindings ("Ctrl+K") and omits it for sequences; the aria-label carries the sequence meaning for the user either way.
  • SHOULD reserve always-on badges for the few primary actions; everything else is a hint, so the chrome stays quiet until asked.

Purpose. A ⌘K quick-open for every operator action. Built on the native <dialog> (focus-trap, Esc, backdrop). The search field is a combobox driving a listbox: focus stays in the input while / move the active option (aria-activedescendant), Enter runs it. Live query filter; each row can show a keycap hint.

const commands = [
{ id: "incident", label: "Open incident", hint: "I", group: "action" },
{ id: "theme:light", label: "Theme: Light", hint: "2", group: "theme" },
];
<CrPalette open={open} commands={commands} onRun={run} onClose={close} />
// host binds the opener
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); open = !open; }
  • MUST drive selection with aria-activedescendant (focus stays in the input), and give each option role="option" + aria-selected.
  • SHOULD filter on both label and group, and reset the query + active row each time it opens.
  • NEVER trap the user — Esc and the backdrop always close it (the dialog owns this).

Purpose. An inline callout keyed to a signal (Law 2) — a left brush-bar in the signal hue, parted from the lit field by a hard seam (Law 1) so the bar reads as inset into the casing. err announces assertively (role=alert); the rest are polite.

The icon is a severity shape (Law 4), not a coloured square: side-count encodes how much the alert needs the operator, orthogonally to hue — so the reading survives phosphor’s single colour and colour-blind viewers.

signalkeyshapeseverity
work--sig-workpentagon ⬠ (5)working
wait--sig-waitdiamond ◆ (4)attend
done--sig-donehexagon ⬡ (6)nominal
err--sig-errtriangle ▲ (3)act now
<CrAlert signal="wait" title="Scheduled maintenance" message="Workers restart at 02:00 UTC." dismissible />
  • MUST map signal to a real state; err uses role="alert" + assertive.
  • SHOULD carry a title only when it adds information — a one-line notice is fine as message alone.

The dismiss ✕ is the shared control described under Dismiss control.


Purpose. One grouped-choice control for BOTH types — single choice (type="radio", the default) or multi-choice (type="checkbox") — rendering CrChoice (native inputs) so each type gets the keyboard model its role requires. Square marks (radius 0) — a filled inner square, not a dot.

type="radio" (default)type="checkbox"
rolesradiogroup / native radiosgroup / native checkboxes
selectionvalue: stringonChangevalues: string[]onChangeMany
keyboardone tab stop; arrows move selectioneach box tabbable; arrows inert
<CrChoiceGroup label="stage" value={stage}
options={[{ value: "queue", label: "queue" }, { value: "run", label: "run" }]}
onChange={setStage} />
<CrChoiceGroup type="checkbox" label="signals" values={signals}
options={[{ value: "err", label: "err" }, { value: "warn", label: "warn" }]}
onChangeMany={setSignals} />
  • MUST keep it controlled — value/onChange for radio, values/onChangeMany for checkbox.
  • MUST let the platform drive the radio keyboard model: the inputs share one name, so the browser supplies roving tabindex and arrow selection. Do NOT add a JS key handler — native role="radio" inputs already get this for free, and a hand-rolled roving-tabindex port would be dead code here.
  • NEVER apply the radio arrow-key model to checkboxes — independent tabbing is required, and arrow-key selection would be an accessibility defect.
  • invalid is ARIA-only (sets aria-invalid); visual error styling comes from a wrapping CrField.

Purpose. A numeric operator control (threshold, interval). A styled native <input type=range>, so keyboard (arrows, Home/End, PageUp/Down) and screen-reader support come for free.

<CrSlider value={refresh} min={5} max={120} step={5} label="Refresh interval" onChange={setRefresh} />
  • MUST give it an aria-label (or visible label) — the value alone isn’t a name.

Purpose. Task progress and capacity / utilisation in one square, hard-edged bar keyed to a signal tone. Determinate fills to value/max; indeterminate runs an animated hazard sweep and drops the numeric ARIA values. An optional label renders inline before the track, which covers the capacity reading that the removed Meter used to serve.

<CrProgress value={64} label="Indexing" />
<CrProgress indeterminate signal="wait" label="Syncing" />
<CrProgress value={72} signal="idle" label="cpu" />
<div class="cr-progress">
<span class="cr-progress__label">cpu</span>
<span class="cr-progress__track" role="progressbar" aria-valuenow="72" aria-valuemin="0" aria-valuemax="100" aria-label="cpu">
<span class="cr-progress__fill" style="width:72%"></span>
</span>
</div>
  • MUST use role="progressbar"; set aria-valuenow/min/max only when determinate. Both stop animating under prefers-reduced-motion.
  • MUST carry the numeric value in ARIA — the fill width alone is not accessible. Tone (--work/--wait/--done/--err/--idle) follows Law 2.

Charts — Sparkline · Line · Bar · Stacked

Section titled “Charts — Sparkline · Line · Bar · Stacked”

The chart family renders the same house rules across four forms (from the data-viz method, tuned to the signal palette): thin marks, a crisp non-scaling 2px stroke (vector-effect="non-scaling-stroke" so lines stay sharp at any width), a recessive grid (--ink mixed to ~16%), baseline-anchored bars with rounded data-ends, a 2px surface gap between fills, labels/legends in text ink — never the series colour, and one y-axis, ever (no dual-scale charts — split into two figures instead).

Numbered y-axis. Line and bar charts derive a “nice” scale — the domain is rounded out to whole 1/2/5×10ᵏ tick steps (the classic Heckbert algorithm), so the gridlines land on human numbers rather than raw data extremes. Each gridline is labelled at the left gutter in monospace tick ink, formatted compactly (1.5k, 2M); pass unit to suffix them ("ms", "%"). The axis is on by default — set axis={false} for a sparkline-style bare plot. Force the extents with min/max (line) or max (bar) to pin the scale; otherwise it fits the data (and the bar target). The same nice-scale math backs the static gallery SVGs, so the hand-rendered and live charts share one y-axis.

Log y-scale (line chart). Set yScale="log" for a base-10 axis — the right choice when a metric spans orders of magnitude (latency p50→p99, payload sizes). The domain snaps to powers of ten and ticks are 1·2·5×10ᵏ (few decades) or plain powers of ten (many), labelled with the usual compact format (10, 1k, 100k). Log needs positive data: values ≤ 0 are clamped to the axis floor, and a series with no positive values falls back to the linear scale. Bar charts stay linear — they’re baseline-anchored at zero, where log is undefined.

Continuous / calendar x-axis (line chart). By default the line chart’s x-axis is categorical — samples are evenly spaced and labelled from labels. Pass an x array of numbers (parallel to each sample index) to switch to a real continuous x-scale: points then sit at their value, faint vertical gridlines mark nice x ticks, and the crosshair snaps to the nearest sample by x-distance.

Add xTime to treat x as epoch-ms and get a timezone-aware calendar axis. The tick granularity auto-scales to the span — clock intervals (…30s · 1m · 1h) for sub-day ranges, then day → week (Mondays) → month → year boundaries for longer ones — and every calendar tick lands on a real boundary in xZone (an IANA zone, default "UTC"), DST included. So a five-month chart ticks on the 1st of each month in local time, a multi-week chart on local Mondays, a multi-year chart on Jan 1. Labels format to the unit (09:30, 3 Mar, Mar, Jan '25, 2025); the hover tooltip shows a fuller stamp (3 Mar 09:15). This uses the built-in Intl zone database (no date library, no bundle cost) and is shared with the static gallery via @alebianco/cr-design-system/time-scale (timeTicks(lo, hi, { zone, target, locale, week, fiscalStart })), so hand-rendered and live charts agree.

The calendar can be expressed the way a team reads it, via three more props (all optional, defaults reproduce a plain Gregorian axis):

  • xLocale — month-name language for the ticks: "en" (default) or "it" (gen · feb · mar …). Day-first ordering either way.
  • xWeek — weekly ticks as dates (3 Mar, default) or ISO week numbers (W10, weeks still starting Monday).
  • xFiscalStart — fiscal year start month 1–12 (default 1 = calendar). Year and quarter ticks then anchor to that month and label FY/Q — e.g. xFiscalStart={4} ticks quarters on Apr/Jul/Oct/Jan as Q1 FY26 · Q2 · Q3 · Q4 and years on 1 April as FY26 (named by the calendar year the fiscal year ends).

It’s a Gregorian calendar/clock scale. For a genuinely non-time axis — ordinal buckets or a 4-4-5 retail calendar — pre-compute and pass x + labels yourself.

Gap-collapse / market-hours axis (line chart). Set xBreak on a continuous x (sorted ascending) to collapse idle gaps — nights, weekends, holidays — so session data reads without empty stretches, the way a trading chart does. Any gap larger than xBreakGap (default ~3× the typical sample gap) is compressed and marked with a dashed break line; the axis then shows one tick per session day. It works purely from the data’s own gaps — if your series only carries session points, the closed periods vanish automatically, no exchange calendar needed. (This is an ordinal compression: spacing within a session stays proportional, but a collapsed gap is not to scale — that’s the point.)

Custom labels — the xFormat escape hatch. When none of the presets fit (ISO label variants, relative “T‑3d” stamps, a 4‑4‑5 retail label, a non-EN/IT locale), pass xFormat={(value) => string}. It relabels the chosen tick positions and the hover stamp; positions still come from the active scale. xFormat sits at the top of label precedence — it overrides xLocale / xWeek / xFiscalStart and the clock format. The same hook exists on the util: timeTicks(lo, hi, { format }).

Axis options don’t collide — precedence and applicability. The props read as a flat bag, but they resolve in a fixed order, and options outside their mode are inert (ignored), never conflicting:

  • Which x-axis: x present → continuous (and labels is ignored); otherwise categorical from labels. xBreak layers gap-collapse on top of a continuous x.
  • Tick text precedence (continuous): xFormat → else xTime calendar labels (themselves shaped by xZone · xLocale · xWeek · xFiscalStart) → else plain numeric. Only one wins; the calendar sub-options apply only under xTime and are no-ops otherwise.
  • Granularity picks the sub-option: at a given span exactly one calendar unit is active, so xWeek (weeks) and xFiscalStart (months/years) never fight — each only affects its own unit. Under xBreak, ticks are per-session-day, so xWeek / xFiscalStart don’t apply.
  • yScale is orthogonal to every x option (it’s the other axis). Log needs positive data and quietly falls back to linear otherwise.

So mixing, say, xWeek="iso" with xFiscalStart={4} is safe: you get ISO weeks when the span is weeks and fiscal quarters when it’s months — never a garbled blend.

Interactive legend (line chart). For ≥ 2 series the legend keys are buttons: click one to isolate/restore that series. Hidden series drop out of the plot, the tooltip, and the auto y-domain — so isolating one refits the y-scale to what’s visible. Keys are keyboard-operable with a visible focus ring and carry aria-pressed; the muted --off state is not colour-alone (opacity + strike). The role="img" + spoken summary lives on the graphic wrapper, not the figure, so the interactive legend is never nested inside an image subtree (axe nested-interactive-clean). The static gallery renders the keys as buttons for markup parity; the toggling itself is a runtime behaviour of the compiled component.

Series colour follows the entity: pass a signal tone, or omit it to take the next hue in a fixed categorical order (work · accent-2 · accent · wait · done) — never cycled, so a filtered-out series never repaints the survivors. That order is chosen so adjacent hues stay separable under colour-vision deficiency (validated with the data-viz palette checker against each theme surface). Every figure is role="img" with a spoken aria-label summary; the SVG itself is aria-hidden. A legend is present whenever there are ≥ 2 series and each bar carries a direct category label — identity is never colour-alone, which is also the required relief for the palette’s few CVD/contrast warnings. Status tones on a stacked bar are always paired with a text label.

Hover layer. Line and bar charts ship a pointer layer by default — progressive enhancement that renders nothing at rest, so it never touches the static page or the a11y tree. On the line chart a pointer snaps a dashed crosshair to the nearest sample, drops a dot on every series, and docks a tooltip reading each series’ value at that x. On the bar chart the nearest bar stays lit while the rest dim, and a tooltip reads its label + value. It stays on under the calm profile (interaction feedback, not idle motion); keyboard / AT users get the same numbers from the figure’s spoken summary.

Palette note. The signal hues are max-neon by design (Law 2), so they sit brighter than a generic mid-lightness categorical band — an accepted house deviation, the same palette the whole system ships and gates via verify:palette. Separation and contrast are what we hold the line on; the always-present legend + labels + spoken summary carry identity regardless.

Purpose. An inline micro line/area for a KPI or table cell — no axes, just the shape of a trend, with a data-end dot. Stretches to fill its box.

<CrSparkline data={[3,5,4,7,6,9,8,12,10,14]} signal="work" area label="p95 latency" />

Purpose. A time series. Numbered nice-scale y-axis, a categorical or continuous/time x-axis, recessive grid, a 2px line + data-end dot per series, and an interactive legend (click to isolate) for ≥ 2 series.

{/* Categorical x-axis (evenly spaced, labelled) */}
<CrLineChart
series={[
{ name: "throughput", data: [12,18,15,22,19,26,24,31], signal: "work" },
{ name: "errors", data: [2,3,2,5,4,3,6,4], signal: "err" },
]}
labels={["09","10","11","12","13","14","15","16"]}
area label="Throughput vs errors" />
{/* Calendar x-axis: samples sit at their timestamp, ticks snap to real
calendar boundaries in the given zone (here, monthly over five months) */}
<CrLineChart
series={[{ name: "budget", data: weekly /* 22 weekly points */, signal: "work" }]}
x={weekTimestamps /* epoch-ms, parallel to each sample */}
xTime xZone="Europe/Rome" unit="%" label="Error budget (5 months)" />
  • One shared y-scale. If two measures differ wildly in magnitude (e.g. throughput vs a 0–100 %), don’t add a second axis — isolate one via the legend, show two charts, or index to a base.

Purpose. Magnitude across categories. Numbered nice-scale y-axis, baseline-anchored bars, rounded data-ends, a 2px gap, optional dashed target line (a budget / SLO), and monospace value + category labels.

<CrBarChart
data={[{label:"eu",value:42},{label:"us",value:31},{label:"ap",value:18},{label:"sa",value:9}]}
target={35} showValues label="Sessions by region" />

Purpose. Composition — a “stacked progress”. One track split into signal-toned segments sized by share, with a legend (label · value · %). Compose several in a column for a per-row comparison (fleet state, error budget, queue mix).

<CrStackedBar
label="Fleet state"
segments={[
{ label: "working", value: 6, signal: "work" },
{ label: "waiting", value: 3, signal: "wait" },
{ label: "done", value: 9, signal: "done" },
{ label: "failed", value: 1, signal: "err" },
]} />

Purpose. A loading placeholder — a blocky pulse (never rounded). Sizes: --line, --text, --block; set the width inline.

<span class="cr-skeleton cr-skeleton--text" style="width:70%"></span>
  • SHOULD mirror the shape of the content it stands in for; MUST be aria-hidden (it carries no information) and it freezes under reduced-motion.

Purpose. A key → value readout for detail panels — dl/dt/dd on a two-column grid, mono/label keys and data-register values.

<dl class="cr-dl">
<dt class="cr-dl__k">worker</dt><dd class="cr-dl__v">nova-01</dd>
<dt class="cr-dl__k">uptime</dt><dd class="cr-dl__v">41h 12m</dd>
</dl>
  • SHOULD keep keys in the label register (mono, uppercase, --muted) and values in the data register — the same split the rest of the system uses.

Purpose. Fold dense sections (logs, config, details). Each header is a button (aria-expanded + aria-controls); the panel is a role=region. single makes it exclusive; //Home/End move between headers (Enter/Space toggle).

<CrAccordion single defaultOpen={[0]} items={[
{ title: "Stack trace", body: "SSEError: stream closed at turn 42" },
{ title: "Config", body: "model=opus · timeout=30s" },
]} />
  • MUST tie header → panel with aria-controls/aria-labelledby and keep aria-expanded in sync; NEVER animate height in a way that breaks reduced- motion (only the chevron rotates, and it freezes under the preference).

Purpose. A generic anchored overlay for arbitrary content. A trigger toggles a floating panel; a transparent full-viewport scrim closes it on outside click, Esc closes and returns focus to the trigger. Use Menu for a list of actions.

Positioning. The panel is collision-aware: on open it anchors to the trigger, flips above when there’s no room below, and shifts horizontally to stay in the viewport (never clipping off-screen), tagging itself with data-placement. The algorithm is exported for your own overlays as @alebianco/cr-design-system/positioncomputePosition (pure geometry, unit- tested), place(anchor, floating, opts), and autoPlace(...) (keeps it pinned on scroll/resize). No dependency on Floating UI.

<CrPopover label="filters ▾" title="Queue filters">
<label><input type="checkbox" class="cr-check" /> failing</label>
</CrPopover>
  • MUST set aria-expanded on the trigger and give the panel an accessible name (title); focus moves into the panel on open.

Purpose. An edge sheet for detail/inspector panels. Built on the native <dialog> (focus-trap, Esc, backdrop); slides from the left or right, full height. Controlled via open (like Modal).

<CrDrawer open={open} side="right" title="cr-1130 · inspect" onClose={close}>
<dl class="cr-dl"></dl>
<CrAccordion single items={sections} />
</CrDrawer>
  • MUST drive it from open and handle onClose (the dialog fires it on Esc and backdrop click); the slide-in animation is off under reduced-motion.

Purpose. A navigation trail. The last crumb is the current page (aria-current="page").

Notes — the separator is a real aria-hidden element between crumbs, defaulting to (the system’s direction marker, shared with List bullets and the Calendar / Carousel next controls). Override it with separator.

<CrBreadcrumb items={[{ label: "control room", href: "#" }, { label: "sessions", href: "#" }, { label: "cr-1130" }]} />
<CrBreadcrumb separator="//" items={[{ label: "hub", href: "#" }, { label: "worker-01" }]} />
  • MUST wrap it in <nav aria-label> and mark the last crumb aria-current (it is not a link).
  • MUST keep the separator aria-hidden — otherwise a screen reader announces it between every crumb.

Purpose. A single choice shown as a connected button bar (filters, scopes). role=radiogroup + roving tabindex (//Home/End) — the same semantics as a radio group, a distinct connected visual.

<CrSegmented value={scope} options={[{ value: "all", label: "all" }, { value: "mine", label: "mine" }]} onChange={setScope} />
  • SHOULD reach for this over a radio group when the options are few, short, and mutually exclusive; keep it to one line.

Purpose. An autocomplete form field: an input (role=combobox) filtering a listbox. Focus stays in the input; / move the active option (aria-activedescendant), Enter selects, Esc closes; a scrim closes on outside click. The active row shows an ascii marker.

<CrCombobox value={worker} options={workers} placeholder="worker…" onChange={setWorker} />
<CrCombobox source={(q) => fetch(`/api/workers?q=${q}`).then(r => r.json())} onChange={setWorker} />
  • Two source modes. Pass a static options list (filtered client-side) or an async source(query) => Promise<{value,label}[]> for a remote lookup — the source filters its own results (and should debounce / order them); the field shows a searching… row while it resolves. The same source model drives CrForm’s autocomplete field kind (see Forms).
  • MUST keep aria-expanded/aria-activedescendant in sync and give each option role="option". It seeds its text from value on mount; selecting emits the value.

Purpose. A number input with /+ steppers, clamped to min/max. The native input keeps its own keyboard; the buttons step by step and disable at the bounds.

<CrNumberField value={retries} min={0} max={10} onChange={setRetries} />
  • MUST clamp on both button and typed input; give it an aria-label.

Character-flavored detail, drawn from the same FUI vocabulary as the decoration layer (references/decoration.md). Structure, never a signal.

ClassWhat
.cr-sep (--dot, --double)a horizontal rule — dashed / dotted / double box-line
.cr-sep-labela labeled rule — ── LABEL ── (dashed flanks, mono label)
.cr-list (--dot --tick --plus)an ascii-marker list — / · / » / + before each item
.cr-leadera dot-leader row — label ········· value (__k / __fill / __v)
<p class="cr-sep-label">recent events</p>
<ul class="cr-list cr-list--tick"><li class="cr-list__item">SSE closed · retry 3/5</li></ul>
<div class="cr-leader"><span class="cr-leader__k">uptime</span><span class="cr-leader__fill"></span><span class="cr-leader__v">41h 12m</span></div>
  • SHOULD keep these to structure and dead space — the markers are ink/--muted, never a signal hue used to imply state.

Purpose. A rich card revealed on hover/focus — like Tooltip but for structured content (a stat block, a preview). CSS-driven with an open delay; the trigger is focusable so keyboard users get it too. For plain text use Tooltip; for actions use Menu.

Positioning. Collision-aware, same algorithm as Popover and Menu: on reveal (pointer hover or keyboard focus) the panel anchors to the trigger via placement (${side} or ${side}-${align}, default "bottom-start"), flips to the opposite side when there’s no room, and shifts along the cross axis to stay in the viewport, tagging itself with data-placement.

<CrHoverCard label="health" title="Fleet health" placement="bottom-start">
<dl class="cr-dl"><dt class="cr-dl__k">workers</dt><dd class="cr-dl__v">4 online</dd></dl>
</CrHoverCard>
  • SHOULD keep the content glanceable; the card is supplementary, not a place for primary actions (it dismisses on blur).

Purpose. Hierarchical data — a worker→session fleet, a config tree. role=tree rendered as a flat list of the currently-visible rows (each with aria-level / aria-expanded). Full keyboard: //Home/End move, expands or steps in, collapses or steps out, Enter/Space toggle+select.

<CrTree label="Fleet" defaultExpanded={["nova"]} nodes={[
{ id: "nova", label: "nova (pool)", children: [{ id: "nova-01", label: "nova-01" }] },
]} />
  • MUST keep aria-level/aria-expanded correct and a single tab stop (roving tabindex). Selecting a node emits onSelect.

Purpose. A styled native datetime-local / date / time input — the browser owns the picker, keyboard, and locale.

<CrDateTime kind="datetime-local" value={startAt} onChange={setStartAt} />
  • MUST give it an aria-label (or a visible label). Prefer native over a custom calendar unless you truly need one.

Purpose. A proper form field for cron scheduling: a real <label for> + input + quick presets + a live human-readable readout. The translation is injected — the host computes it (e.g. with cronstrue) so the design system stays parser-free — and passed as description when the expression parses, or as error when it doesn’t. Validity is message-driven exactly like CrField: there is no hand-set invalid boolean; error sets aria-invalid, shows the message (role="alert"), and links it via aria-describedby. id is required (it ties the label, input and messages together); required, disabled and onBlur behave as on any field.

// host
const d = (() => { try { return { text: cronstrue.toString(cron), bad: false }; }
catch { return { text: "unrecognized cron expression", bad: true }; } })();
<CrCronField
id="restart-cron" label="Restart schedule" value={cron} onChange={setCron}
description={d.bad ? undefined : d.text} // readout when valid
error={d.bad ? d.text : undefined} // validation message when not
presets={[{ label: "nightly 2am", cron: "0 2 * * *" }]} />
  • SHOULD compute the translation reactively (useComputed$ in Qwik, useMemo in React) so it tracks the value, routing it to description or error.
  • MUST NOT hand-set validity — drive it from the parser’s result via error, the same contract as CrField / CrForm.

The interactive widgets follow the WAI-ARIA patterns, so they work without a mouse:

WidgetKeys
Tabsroving tabindex — / (and /) move, Home/End jump to ends; only the active tab is in the tab order
Menutrigger opens on click or ; then / move, Home/End jump, Esc closes and returns focus to the trigger; Enter/Space select
Tablesortable headers are real <button>s (operable with Enter/Space), selection checkboxes are in the tab order
Command palette⌘K/Ctrl+K opens; //Home/End move the active option, Enter runs, Esc closes (focus stays in the search field)
Tree//Home/End move; expands or steps into children, collapses or steps to parent; Enter/Space toggle+select
Accordion//Home/End move between headers; Enter/Space toggle a panel
Popover / DrawerEsc closes (drawer traps focus natively); popover returns focus to its trigger
Segmented controlroving tabindex — //Home/End move and select (radiogroup semantics)
Combobox/ move the active option, Enter selects, Esc closes; focus stays in the input
Choice group (type="radio")roving tabindex — /// move and select; only the checked radio is tabbable
Choice group (type="checkbox")no roving tabindex — every box is independently tabbable, arrows are inert
Slidernative range — / step, Home/End to ends, PageUp/PageDown jump
Modal / Switch / Paginationnative focus-trap (dialog), Space toggle, Tab between page buttons

Focus is always visible (*:focus-visible → a --sig-work outline, system-wide).

A hint bubble revealed on hover and keyboard focus, wired to its trigger with aria-describedby so it’s announced without stealing focus. The reveal is pure CSS (:hover / :focus-within) — no JS state — so the plain HTML markup below still works in a server-rendered page with no component at all; the CSS positions the bubble above the trigger as a fallback in that case.

Positioning. When the component runs, collision-aware placement layers on top of that CSS fallback — same algorithm as Popover, Menu and Hover card: on reveal (pointer hover or keyboard focus) the bubble anchors to the trigger via placement (${side} or ${side}-${align}, default "top" — tooltips conventionally sit above), flips to the opposite side when there’s no room, and shifts along the cross axis to stay in the viewport, tagging itself with data-placement.

<span class="cr-tooltip">
<span class="cr-tooltip__trigger" tabindex="0" aria-describedby="tt-drift">drifting</span>
<span class="cr-tooltip__bubble" role="tooltip" id="tt-drift">latency &gt; SLA for 3 turns</span>
</span>
<CrTooltip id="tt-drift" label="latency > SLA for 3 turns" placement="top">drifting</CrTooltip>
  • MUST give the trigger tabindex="0" (or use a natively focusable element) so the hint is reachable by keyboard, and point aria-describedby at the bubble’s id.
  • MUST keep the bubble a sibling of the trigger, not a child — nesting folds the hint into the trigger’s accessible name.
  • SHOULD keep tooltips short; anything longer than a line belongs in a Panel or a Modal, not a hovering bubble.

Neo-print / CRT grain for hardware surfaces (a bezel, screen, or hero) — never a flat content field (Law 6). Theme-keyed via the texture tokens (references/tokens.md).

ClassTexture
.cr-tex--halftonedot pattern
.cr-tex--ditherordered 1-bit checker
.cr-tex--scanCRT scanlines
.cr-tex--glassscanlines + halftone (the house “aged glass” wash)
<div class="cr-bezel cr-tex--glass cr-anim-scan">
<div class="cr-bezel__screen">&gt; streaming · 14 sessions</div>
</div>
  • MUST apply only to hardware; a textured flat panel violates Law 6.
  • NEVER put a .cr-tex--* class on .cr-bezel__screen. The screen already paints the house halftone itself, on the glass above the readout (.cr-bezel__screen::after) — adding a utility stacks a second texture layer behind the text, and because the utility carries opacity on the element it fades the readout along with the grain. Texture the bezel (the hardware) and leave the screen alone.
  • The utilities set opacity on the element, so apply them to a surface whose own content should fade with the grain — a decorative hardware panel, not a content-bearing readout. To texture over content, use an ::after overlay (inset: 0; pointer-events: none;) the way the screen does.
  • Ambient loop classes (.cr-anim-scan/-pulse/-drift/-flick) are documented in references/motion.md — low, slow, hardware-bound, reduced-motion-off.

A retro-futuristic cyber-sigil generated from a seed — a per-entity identity mark that pairs with the pixel-cat. Full contract in references/seeded-sigil.md.

import { CrSigil } from "@alebianco/cr-design-system/react";
<CrSigil seed="nova-01" state="working" />
  • MUST key the hue to a signal (Law 2); the glyph is identity + state.
  • NEVER use it as the only affordance for an action — it is a mark, not a button.

A shape channel beside the colour channel (Law 4): a regular polygon’s side-count encodes danger/focus inversely — fewer sides = more urgent. Colour says what state; shape says how much it needs you. Crucially the meaning is in the geometry, so it reads in the phosphor CRT (one colour) and for colour-blind operators — it is the non-colour backup the a11y contract requires.

<span class="cr-sev cr-sev--crit" role="img" aria-label="critical"></span> <!-- ▲ 3 -->
<span class="cr-sev cr-sev--warn" role="img" aria-label="attend"></span> <!-- ◆ 4 -->
<span class="cr-sev cr-sev--work" role="img" aria-label="working"></span> <!-- ⬠ 5 -->
<span class="cr-sev cr-sev--ok" role="img" aria-label="nominal"></span> <!-- ⬡ 6 -->
<span class="cr-sev cr-sev--idle" role="img" aria-label="idle"></span> <!-- ● ∞ -->
import { CrShape } from "@alebianco/cr-design-system/react";
<CrShape severity="crit" label="build failing" />
  • MUST give it an accessible label — shape/colour is never the only carrier.
  • MUST keep the scale monotonic: triangle is never calm, circle never critical.
  • Colour defaults to the matching signal; override with --cr-sev-fill to decouple the two channels.

Industrial detail that proves there is real hardware (Law 6) — apply on a bezel, rail, or masthead, never on a flat data field, and keep it aria-hidden (it carries nothing a label doesn’t).

ClassPart
.cr-rivet · --hex · --slotround rivet · hex bolt · slot screw
.cr-screw · --xslot screw · phillips (cross) screw
.cr-boltsquare bolt head
.cr-led · --wait/-done/-err/-idleindicator LED, keyed to a signal (solid, no glow)
.cr-vent · .cr-grillelouvred vent (horizontal) · grille (vertical)
.cr-portconnector port
.cr-stripehazard tape (--sig-wait diagonal)
.cr-seampanel seam (a hairline groove)
.cr-platestamped ID plate (UNIT · CR-00 · REV.C)
.cr-tallytally-mark count readout
  • MUST confine chrome to hardware surfaces; a riveted flat panel is noise.
  • SHOULD use it sparingly — one plate, a few rivets. Chrome is seasoning.

For a whole varied hardware bar, CrChrome paints a seeded pixel-art metal strip — a deterministic graduated scale (minor index ticks with taller major graduations), panel seams, a registration mark, wear scratches, and one indicator LED. Same seed → same strip, so a rack/unit gets a stable, distinct face (like the seeded cat/sigil).

The vocabulary is measurement marks, not fixings: it deliberately does not paint rivets, hex bolts or screw heads, which read as a novelty machine-panel skin rather than as an instrument face.

import { CrChrome } from "@alebianco/cr-design-system/react";
<CrChrome seed="nova-rack" width={440} />
  • Decorative hardware (Law 6); it is aria-hidden/role=img with the seed as its name and carries no information a label doesn’t.
  • NEVER put it behind data; it is a bezel/rail/rack surface, not a content field.

Beyond the halftone dots: .cr-tex--cross is a ±45° crosshatch (Law 4 diagonals as grain), and .cr-tex--duo is a duotone ordered dither mixing two signals (--cr-duo-a / --cr-duo-b, default accent + accent-2) — “cross-colours”. Both are hardware-only like the rest of the .cr-tex--* family. For freeform symbol / ASCII dithering, paint it on a <canvas> (the seeded-cat / sigil engine) — CSS covers the regular patterns; canvas covers the generative ones.

The one sanctioned rule-break per screen (Law 9). .cr-breach licenses the forbidden vocabulary on a single element — a soft corner (--breach-radius), a rotating neon gradient rim with bright spots riding the border, and a dual-hue glow in the house magenta → acid neon pairing (--cr-breach / --cr-breach-2), plus an optional interior --wash — to spotlight the most exceptional thing. The interior stays dark and legible; the strike is the rim + glow. Rotation speed is --breach-spin (default 9s), off under reduced motion. It only reads because everything around it obeys, so use it at most once per screen.

<div class="cr-breach cr-breach--wash cr-breach--alive" style="background:var(--panel)">
<div class="cr-masthead__eyebrow">Milestone</div>
<h2 class="cr-masthead__title">Sprint shipped</h2>
<p>14 sessions · 0 failing · on time</p>
</div>
<span class="cr-blob" aria-hidden="true"></span> <!-- standalone soft accent -->
import { CrBreach } from "@alebianco/cr-design-system/react";
<CrBreach signal="done" wash alive>…the one exceptional thing…</CrBreach>
ClassEffect
.cr-breachsoft corner + neon gradient rim + dual-hue glow (magenta → acid)
.cr-breach--washtints the interior with the pair (kept dark; --ink stays legible)
.cr-breach--aliveslow breathing dual glow (off under reduced motion)
.cr-breach--work/-wait/-done/-err/-accent2re-key the primary hue to a signal
.cr-bloba standalone soft luminous accent (decorative, aria-hidden)
  • MUST use one breach per screen, keyed to a signal, with everything else hard-edged.
  • MUST keep breach text within contrast; the --alive glow honors reduced motion.
  • NEVER breach data, tables, dense lists, or routine chrome — the breach is for the exceptional only.

CrIcon is the house operational glyph set — the one place the system reaches for an icon instead of a text glyph, a canvas sigil, or ASCII.

Contract. Every icon is 24×24, drawn with a single 2px stroke in currentColor (no fill), with square caps and miter joins so the geometry stays hard-edged and consistent with the neobrutalist line. Icons inherit text colour and size on the space grid via size (default 20).

Accessibility. Decorative by default (aria-hidden); pass label to expose the icon as an image with an accessible name. Pair an icon-only control with a label or visually-hidden text.

API. name (glyph id), size (px, default 20), label (optional accessible name), set ("cr" default · "pixel"). The set: play · pause · stop · retry · deploy · scan · search · alert · error · done · clock · cpu · logs · filter · sliders · close · chevron · plus · minus · trash · external · copy · session · menu. Add one by adding a single-d, square-geometry path to the map in components/CrIcon.lite.tsx.

Packs (set) — the soft escape hatch. set="cr" (default) is the hand-authored stroked geometric set above. set="pixel" swaps in a pixel-art pack built offline from Iconify’s pixelarticons (build/build-icons.mjslib/icons/pixel.ts, guarded by pnpm run verify:icons) — filled instead of stroked, same 24×24 grid, same names, still a single <path> so it ports to all six targets (no icon font, no runtime fetch). Use it to give one theme or subtree a softer register:

<section data-theme="phosphor">
<CrIcon name="deploy" set="pixel" label="deploy" />
</section>

Per the design language it is opt-in and never mixed with the geometric set on one surface; a name missing from the pixel pack falls back to the house glyph.

Purpose — show progress through a multi-step flow (the shape the forms guidance points to when a long form is split into steps).

Anatomy — an ordered list (<ol>); each item has a numbered/checked dot and a label (optional hint). Steps are upcoming · active · done by index vs active; the current item carries aria-current="step" and done items show a check. Pass onStep to make each step a <button> (a navigable stepper); omit it for a read-only indicator.

<CrStepper steps={[{ label: "Source" }, { label: "Limits" }, { label: "Review" }]} active={1} />
<CrStepper steps={steps} active={i} onStep={(n) => goTo(n)} />

Tokens--sig-work (active dot), --sig-done (done dot), --muted (upcoming), --border (connector). A11y — ordered list; aria-current="step" on the active step; navigable variant uses real buttons (native focus/activation).

Purpose — enter a one-time code / PIN as a row of single-digit cells that behave as one field.

Behaviour — typing a digit advances focus; Backspace on an empty cell steps back; / move; a paste distributes across cells. length sets the cell count (default 6). onChange fires with the partial code, onComplete when full.

<CrPinInput length={6} onComplete={(code) => verify(code)} />

Tokens--panel-2 (cell fill), --border, --sig-work (focused cell). A11yrole="group" with a label (“Verification code”); each cell is a numeric input with its own “Digit N” label; the first opts into autocomplete="one-time-code" so platforms can offer the SMS code.

Purpose — build a set of short tokens (labels, regions, emails).

Behaviour — type then Enter or comma to add; Backspace on an empty field removes the last tag; each tag has its own remove button. Duplicates are ignored. value seeds the tags; onChange reports the array.

<CrTagsInput label="Regions" value={["eu-west"]} onChange={(tags) => setRegions(tags)} />

Tokens--panel (tag fill), --border, --sig-err (remove hover). A11yrole="group" with a label; the entry is a labelled input; every remove button names its tag (Remove <tag>).

Purpose — an input flanked by decorative prefix / suffix addons (a protocol, a currency, a unit).

Notes — addons are aria-hidden so they don’t muddy the accessible name; give the field a label (rendered as aria-label). For a validated field with its own label/hint/error, use Field or Form instead.

<CrInputGroup label="Endpoint" prefix="https://" placeholder="eu.example.com" />
<CrInputGroup label="Memory" suffix="GB" type="number" />

Tokens--panel-2 (addon fill), --border, --muted (addon text).

Purpose — represent a person or entity with an image, falling back to initials.

Anatomy — with a src it renders <img alt={name}>; without one the wrapper becomes role="img" with aria-label={name} over the derived initials (which are aria-hidden, so the name isn’t announced twice). An optional presence status dot is a labelled role="img". size is sm · md · lg.

<CrAvatar name="Ada Lovelace" src="/ada.png" status="online" />
<CrAvatar name="Grace Hopper" status="idle" size="lg" />

Tokens--panel-2 (fallback fill), --border (ring), --sig-done/--sig-wait/--sig-err/--muted (status).

Purpose — signal an indeterminate wait (unknown duration). For a known fraction, or a static capacity reading, use Progress with a label.

Notes — the wrapper is role="status" with an accessible label (default “Loading”) so assistive tech announces the wait; the ring is aria-hidden and its spin honours prefers-reduced-motion. size is sm · md · lg. signal keys the cells to the canonical vocabulary — work · wait · done · err · idle, the same tone set as Progress — and defaults to work. Tone is a redundant cue: the accessible name carries the meaning.

<CrSpinner label="Provisioning session" />
<CrSpinner label="Retrying upload" signal="err" />

Tokens--cr-spinner-accent (cells, defaults to --sig-work), --sig-work/--sig-wait/--sig-done/--sig-err/--sig-idle (per signal).

Purpose — a scroll container that keeps the Control Room look instead of the OS default scrollbars (thin, inked track, neon thumb).

Notes — the styling is pure CSS (scrollbar-width + ::-webkit-scrollbar); content scrolls natively. The container is tabindex=0 so it’s keyboard-scrollable, and when you pass a label it becomes a named role="group" so assistive tech announces it. axis is y (default) · x · both; maxHeight caps the scroll axis.

<CrScrollArea label="Log output" maxHeight="16rem">{lines}</CrScrollArea>

Tokens--sig-work (thumb), --panel-2 (track).

Purpose — split a region into two panes with a divider the operator can drag (log + detail, tree + editor).

Anatomy — pass the two panes as children; a CSS grid sizes the first to the current split and the second fills the rest. The handle is overlaid at the split line (no markup injected between your panes) and is a WAI-ARIA window splitter: role="separator", focusable, aria-orientation + aria-valuenow/ min/max for the leading pane’s percent. / (or / when vertical) resize by 2%, Home/End jump to the clamps. Dragging uses pointer capture, so there are no global listeners and it can’t get stuck. orientation is horizontal (default) · vertical; defaultSize / min / max are percents.

<CrResizable label="Resize log vs detail" defaultSize={40}>
<div>log</div>
<div>detail</div>
</CrResizable>

Tokens--border (divider + frame), --sig-work (active divider). A11y — the handle is a labelled, focusable role="separator" with live aria-valuenow; full keyboard resize.

An interactive multi-select filter pill — several toggle independently (unlike the single-select Segmented or the static Chip). It is a role="checkbox" button; on/off is announced via aria-checked and exposed as data-state. Props: label, pressed, badge?, onToggle, disabled. badge is generic: false/omitted renders nothing, true renders a bare (decorative, aria-hidden) dot for “has matches, count irrelevant”, and any string or number renders verbatim — 0 and "" still render, because an explicit zero is a meaningful filter result. Tokens--cr-togglechip-on-bg / --cr-togglechip-on-fg (the ON state). Per Law 2 the ON colour is the accent (a state), not a per-option identity hue; override --cr-togglechip-on-bg via dt only when a facet genuinely needs its own on-colour. A11y — labelled checkbox semantics; keyboard-operable.

An a11y-correct “+N more” disclosure for truncated lists. With onToggle it’s a <button aria-expanded> that reveals/hides the overflow; without it, an inert count. The accessible name always includes the noun (screen readers never hear a bare “+3”). Props: count, expanded, noun, onToggle?. A11yaria-expanded reflects state; aria-label = “show N more ”.

A relative-time display (“5m ago”, “in 2h”) rendered as a semantic <time> with a machine-readable datetime. The clock is injected (now prop, epoch ms) — never read internally — so SSR and client agree and there’s no hydration flicker; omit now to show the absolute date. Props: time, now?, prefix?. See also utils/duration for the same formatting outside a component.

A star-rating control. Interactive mode is a WAI-ARIA role="radiogroup" with roving tabindex (←/→/Home/End move and select; the value is the number of the focused star); readonly mode is an inert role="img" whose accessible name is the score (“rating: 3 of 5”). The mark is a geometric glyph — filled vs outline , never an icon font — and its colour encodes the value. Props: value, max (default 5), onChange, readonly, disabled, label.

<CrRating value={3} max={5} label="severity" onChange={setV} />
<CrRating value={4} readonly label="score" />

Tokens--cr-rating-on (filled) · --cr-rating-off (empty). Per Law 2 the fill is the accent (a state), not a per-star identity hue. A11y — radiogroup with a single tab stop; readonly collapses to a labelled image.

A vertical event timeline — an ordered list (<ol>) of moments on a rail, each with a signal-coloured node, a machine <time>, a title and optional detail. Presentational; the node colour is the semantic signal (work · done · wait · err), never decoration (Law 2). Props: items ({ time, title, detail?, signal? }[]), label.

<CrTimeline label="incident" items={[
{ time: "09:41", title: "alert raised", signal: "err" },
{ time: "09:43", title: "ack by on-call", signal: "wait" },
{ time: "10:02", title: "mitigated", signal: "done" },
]} />

Tokens--cr-timeline-rail (the connecting rail is chrome). Nodes are signal-driven, so they have no per-component token — retheme through the signal tokens. A11y — a semantic ordered list; nodes are aria-hidden, the title carries the meaning.

A WAI-ARIA toolbar — a labelled group of controls with a single tab stop and roving-tabindex arrow navigation (←/→ horizontal, ↑/↓ vertical, Home/End to the ends). Enter/Space activate the focused control natively. Two modes:

Children mode — wrap your own controls (buttons, links, fields). The first is made the tab stop on mount; arrows move focus and the stop together. Props: label (required), orientation? (horizontal default · vertical), children.

<CrToolbar label="editor actions">
<button class="cr-btn cr-btn--sm">bold</button>
<button class="cr-btn cr-btn--sm">italic</button>
</CrToolbar>

Items + overflow mode — pass items as data and set overflow to get the priority+ pattern: the buttons that don’t fit collapse into a ”⋯ more” menu instead of wrapping. A ResizeObserver measures the bar live and moves the tail into the menu as it narrows; the ”⋯” trigger is a proper aria-haspopup="menu" (↓ opens, ↑/↓/Home/End move, Esc closes). Each item is { id, label, onSelect?, disabled?, danger? }. SSR / no-JS renders every item in the bar (all actions stay reachable), then the measure pass runs on hydrate.

<CrToolbar label="editor actions" overflow items={[
{ id: "bold", label: "bold", onSelect: bold },
{ id: "italic", label: "italic", onSelect: italic },
{ id: "link", label: "link", onSelect: link },
{ id: "code", label: "code", onSelect: code },
// …as many as you like — the tail folds into "⋯ more" when space runs out
]} />

Tokens--cr-toolbar-bg · --cr-toolbar-border (the overflow menu reuses the Menu surface). A11yrole="toolbar" with aria-orientation; roving tabindex keeps the whole bar (including the ”⋯” trigger) one Tab stop; the menu is a labelled role="menu" of role="menuitem"s. Note — overflow mode measures widths via the DOM (a data-w stamp per button) and fills its container (display:flex), so the measurement is stable as items collapse.

A file dropzone over a real native <input type="file"> — click or keyboard opens the picker, drag-and-drop is also accepted, and the dragover state is announced via data-state. The input stays a focusable, labelled control (visually hidden, not display:none) so keyboard and screen-reader users get the native experience; the styled surface is aria-hidden decoration. Props: label (required), accept?, multiple?, disabled?, hint?, files? (names to list), onFiles (fires with the native FileList).

<CrFileUpload label="Drop a CSV or browse" hint="CSV up to 10 MB"
accept=".csv" onFiles={(fl) => ingest(fl)} files={["runs-2026.csv"]} />

Tokens--cr-fileupload-active-border is the dragover accent (a state, Law 2), plus --cr-fileupload-bg/fg/border. A11y — the native input is the control; the label associates the accessible name; focus ring rides the visible surface.

A slide carousel on the WAI-ARIA carousel pattern — a labelled region (aria-roledescription="carousel"), each slide a group announced as “N of M”, previous/next controls, and optional dot indicators (a tablist). The viewport is aria-live="polite" so a slide change is spoken; ←/→ move slides from anywhere inside. Controlled via index/onIndex. Props: slides ({ title, caption? }[]), index?, label (required), onIndex, dots? (default true).

<CrCarousel label="onboarding" index={i} onIndex={setI} slides={[
{ title: "connect", caption: "point at your cluster" },
{ title: "observe", caption: "watch the first signals land" },
]} />

Tokens--cr-carousel-dot-active is the active-dot accent (a state, Law 2), plus --cr-carousel-bg · --cr-carousel-dot. A11y — labelled region + slide groups; prev/next and dots are named; the viewport announces changes.

A month calendar gridrole="grid" with weekday columnheaders and day buttons, roving tabindex (←/→ ±1 day, ↑/↓ ±1 week, Home/End to the week ends, PageUp/PageDown step months, Enter/Space select). Fully controlled and SSR-safe: the displayed month and today are injected props, never read from the clock, so server and client render the same grid. Props: month (YYYY-MM), value? (YYYY-MM-DD), today?, min?/max?, weekStart? ("sunday" default · "monday"), switcher? (default true), yearSpan? (default 8), label, onSelect, onMonthChange.

The header carries a month/year switcher — a month dropdown and a year dropdown beside the prev/next steppers. Every one of the four controls emits onMonthChange with the new YYYY-MM; none of them reads the clock. The year list is derived from the displayed year (yearSpan either side, clamped to min/max), which is what keeps the switcher SSR-safe — there is no “now” in it. Pass switcher={false} for the bare prev/next header.

<CrCalendar month="2026-08" value="2026-08-09" today="2026-08-09"
weekStart="monday" onSelect={setDate} onMonthChange={setMonth} label="run date" />

Tokens--cr-calendar-selected-bg (selected-day fill, a state) · --cr-calendar-today-ring · --cr-calendar-muted (adjacent-month days) · --cr-calendar-bg. A11y — grid semantics with aria-selected, aria-current="date" for today, aria-disabled outside min/max; one tab stop with full keyboard traversal. The month label stays in the a11y tree as an aria-live region behind the switcher, and the two selects are named Month and Year. Hovering the selected day keeps the accent fill (sunk 15% toward --cr-calendar-bg) rather than falling back to the plain hover surface, which is what keeps the selected+hovered numeral above 4.5:1 in all four themes.