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 */Dismiss control
Section titled “Dismiss control”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: inheritandborder-color: currentColor. That is what makes one treatment correct on every host: on a--panelsurface it resolves to--ink; on a flooded toast it resolves to--on-sig/--on-err. A fixed--panel/--borderchip 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-heightrather than inheriting them..cr-alertsets nofont-sizewhile.cr-toastsets--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-heightalone 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
majorweight 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
indexdecorative — it isaria-hidden, a stamp on the casing rather than a datum. A readable unit id goes in theeyebrow, which is real text. - MUST key
toneto 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
bleedmask 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.
Masthead
Section titled “Masthead”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
h1in the display register only. - SHOULD set the eyebrow in
--sig-work(or--stagewhen calm). - SHOULD add
.cr-markto the primary readout for the signature registration ticks (industrial crop marks at opposite corners — structure, not a signal; seedesign-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.
Hero (keyed focal region)
Section titled “Hero (keyed focal region)”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.
Rail + Nav
Section titled “Rail + Nav”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-errfor attention items). - MUST mark the current item
aria-current="page".
SessionRow
Section titled “SessionRow”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
.eventon 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.
StatusDot
Section titled “StatusDot”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-labelnaming 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 —altresolved to--sig-work— so it is nowsignal, withaltbecomingwork. See Law 2 inreferences/design-language.mdfor the canonical vocabulary.
Button
Section titled “Button”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
:activethe 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.
Bezel + Screen
Section titled “Bezel + Screen”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">> 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.
Data grid
Section titled “Data grid”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.rowHeightis a fixed number (fast O(1) path) or a(row, index) => numberfunction 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. EmitsonSelectionChange(keys). - It’s a div-grid (
role="grid"withrow/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-selectedon 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 */Diagonal primitives (Law 4)
Section titled “Diagonal primitives (Law 4)”Four shapes, four fixed meanings. Max 15° off-axis. NEVER decorative.
| Primitive | Meaning | Mechanism |
|---|---|---|
.chev | direction | left-facing CSS triangle before the label |
.notch | state | clip-path corner cut |
.wedge | active-panel focus | accent clip-path wedge on the trailing edge |
.arrowrail span | sequence / pipeline step | chevron-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.
Keyed contact sheet (Law 2)
Section titled “Keyed contact sheet (Law 2)”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.
Drip / error surface (Law 3)
Section titled “Drip / error surface (Law 3)”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.
Glitch tiers — intensity is the readout
Section titled “Glitch tiers — intensity is the readout”Law 3 maps corruption depth to severity, so pick the tier that matches the state rather than dialling a decorative amount:
| Class | Tier | Meaning |
|---|---|---|
.cr-glitch-t1 | split | nominal |
.cr-glitch-t2 | slice | degraded |
| (authored content) | cursed | failed — 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.
EmptyState / ErrorState
Section titled “EmptyState / ErrorState”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.
Seeded pixel-cat
Section titled “Seeded pixel-cat”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.
Composition — an operator’s screen
Section titled “Composition — an operator’s screen”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">> 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.
Form controls
Section titled “Form controls”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.
Instrument shell
Section titled “Instrument shell”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).
Overlays
Section titled “Overlays”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 theopenattribute) so the backdrop and focus-trap engage; the component does this for you. - MUST name the dialog —
titlebecomesaria-label; the✕is labelledClose. - The
::backdropis--massat 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.erruses--on-errtext; the rest use--on-sig. - MUST use
role="alert"+aria-live="assertive"forerr,role="status"politeotherwise — the component picks this fromsignal.
- SHOULD auto-dismiss non-critical toasts (
duration); keep errors sticky so they can’t be missed.
Toast region
Section titled “Toast region”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:
| field | which toast | why |
|---|---|---|
id | oldest (first) member | identity. Stable while the run grows, so the row is patched rather than remounted on a count bump. |
newestId | newest member | dismiss 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
×Ncounteraria-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, anderrannounces assertively. (aria-atomicdefaults tofalse, 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 bytests/cross-fw-contract.test.mjsacross 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-expandedon the trigger androle="menuitem"on each item. - SHOULD reserve
--dangerfor destructive actions only (Law 2).
Pagination
Section titled “Pagination”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.
Key hint
Section titled “Key hint”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 (defaultAlt) via the headlessCrKeyHintsbehavior.
<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-keyshortcutson 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 anaria-labelof 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 emitsaria-keyshortcutsonly for single-step bindings ("Ctrl+K") and omits it for sequences; thearia-labelcarries 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.
Command palette
Section titled “Command palette”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 openerif ((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 optionrole="option"+aria-selected. - SHOULD filter on both label and group, and reset the query + active row each time it opens.
- NEVER trap the user —
Escand 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.
signal | key | shape | severity |
|---|---|---|---|
work | --sig-work | pentagon ⬠ (5) | working |
wait | --sig-wait | diamond ◆ (4) | attend |
done | --sig-done | hexagon ⬡ (6) | nominal |
err | --sig-err | triangle ▲ (3) | act now |
<CrAlert signal="wait" title="Scheduled maintenance" message="Workers restart at 02:00 UTC." dismissible />- MUST map
signalto a real state;errusesrole="alert"+ assertive. - SHOULD carry a title only when it adds information — a one-line notice is fine
as
messagealone.
The dismiss ✕ is the shared control described under Dismiss control.
Choice group
Section titled “Choice group”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" | |
|---|---|---|
| roles | radiogroup / native radios | group / native checkboxes |
| selection | value: string → onChange | values: string[] → onChangeMany |
| keyboard | one tab stop; arrows move selection | each 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/onChangefor radio,values/onChangeManyfor 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 — nativerole="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.
invalidis ARIA-only (setsaria-invalid); visual error styling comes from a wrappingCrField.
Slider
Section titled “Slider”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.
Progress
Section titled “Progress”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"; setaria-valuenow/min/maxonly when determinate. Both stop animating underprefers-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 month1–12(default1= calendar). Year and quarter ticks then anchor to that month and labelFY/Q— e.g.xFiscalStart={4}ticks quarters on Apr/Jul/Oct/Jan asQ1 FY26 · Q2 · Q3 · Q4and years on 1 April asFY26(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:
xpresent → continuous (andlabelsis ignored); otherwise categorical fromlabels.xBreaklayers gap-collapse on top of a continuousx. - Tick text precedence (continuous):
xFormat→ elsexTimecalendar labels (themselves shaped byxZone·xLocale·xWeek·xFiscalStart) → else plain numeric. Only one wins; the calendar sub-options apply only underxTimeand are no-ops otherwise. - Granularity picks the sub-option: at a given span exactly one calendar unit is
active, so
xWeek(weeks) andxFiscalStart(months/years) never fight — each only affects its own unit. UnderxBreak, ticks are per-session-day, soxWeek/xFiscalStartdon’t apply. yScaleis 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.
Sparkline
Section titled “Sparkline”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" />Line chart
Section titled “Line chart”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.
Bar chart
Section titled “Bar chart”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" />Stacked bar
Section titled “Stacked bar”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" }, ]} />Skeleton
Section titled “Skeleton”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.
Data list
Section titled “Data list”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.
Accordion
Section titled “Accordion”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-labelledbyand keeparia-expandedin sync; NEVER animate height in a way that breaks reduced- motion (only the chevron rotates, and it freezes under the preference).
Popover
Section titled “Popover”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/position — computePosition (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-expandedon the trigger and give the panel an accessible name (title); focus moves into the panel on open.
Drawer
Section titled “Drawer”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
openand handleonClose(the dialog fires it onEscand backdrop click); the slide-in animation is off under reduced-motion.
Breadcrumb
Section titled “Breadcrumb”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 crumbaria-current(it is not a link). - MUST keep the separator
aria-hidden— otherwise a screen reader announces it between every crumb.
Segmented control
Section titled “Segmented control”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.
Combobox
Section titled “Combobox”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
optionslist (filtered client-side) or an asyncsource(query) => Promise<{value,label}[]>for a remote lookup — the source filters its own results (and should debounce / order them); the field shows asearching…row while it resolves. The same source model drivesCrForm’sautocompletefield kind (see Forms). - MUST keep
aria-expanded/aria-activedescendantin sync and give each optionrole="option". It seeds its text fromvalueon mount; selecting emits the value.
Number field
Section titled “Number field”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.
ASCII separators & lists
Section titled “ASCII separators & lists”Character-flavored detail, drawn from the same FUI vocabulary as the decoration
layer (references/decoration.md). Structure, never a signal.
| Class | What |
|---|---|
.cr-sep (--dot, --double) | a horizontal rule — dashed / dotted / double box-line |
.cr-sep-label | a labeled rule — ── LABEL ── (dashed flanks, mono label) |
.cr-list (--dot --tick --plus) | an ascii-marker list — ▸ / · / » / + before each item |
.cr-leader | a 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.
Hover card
Section titled “Hover card”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-expandedcorrect and a single tab stop (roving tabindex). Selecting a node emitsonSelect.
Date-time
Section titled “Date-time”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.
Cron field
Section titled “Cron field”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.
// hostconst 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,useMemoin React) so it tracks the value, routing it todescriptionorerror. - MUST NOT hand-set validity — drive it from the parser’s result via
error, the same contract asCrField/CrForm.
Keyboard navigation
Section titled “Keyboard navigation”The interactive widgets follow the WAI-ARIA patterns, so they work without a mouse:
| Widget | Keys |
|---|---|
| Tabs | roving tabindex — ←/→ (and ↑/↓) move, Home/End jump to ends; only the active tab is in the tab order |
| Menu | trigger opens on click or ↓; then ↑/↓ move, Home/End jump, Esc closes and returns focus to the trigger; Enter/Space select |
| Table | sortable 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 / Drawer | Esc closes (drawer traps focus natively); popover returns focus to its trigger |
| Segmented control | roving 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 |
| Slider | native range — ←/→ step, Home/End to ends, PageUp/PageDown jump |
| Modal / Switch / Pagination | native focus-trap (dialog), Space toggle, Tab between page buttons |
Focus is always visible (*:focus-visible → a --sig-work outline, system-wide).
Tooltip
Section titled “Tooltip”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 > 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 pointaria-describedbyat the bubble’sid. - 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.
Texture utilities (hardware only)
Section titled “Texture utilities (hardware only)”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).
| Class | Texture |
|---|---|
.cr-tex--halftone | dot pattern |
.cr-tex--dither | ordered 1-bit checker |
.cr-tex--scan | CRT scanlines |
.cr-tex--glass | scanlines + halftone (the house “aged glass” wash) |
<div class="cr-bezel cr-tex--glass cr-anim-scan"> <div class="cr-bezel__screen">> 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 carriesopacityon the element it fades the readout along with the grain. Texture the bezel (the hardware) and leave the screen alone. - The utilities set
opacityon 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::afteroverlay (inset: 0; pointer-events: none;) the way the screen does. - Ambient loop classes (
.cr-anim-scan/-pulse/-drift/-flick) are documented inreferences/motion.md— low, slow, hardware-bound, reduced-motion-off.
Sigil (seeded pixel-glyph)
Section titled “Sigil (seeded pixel-glyph)”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.
Severity shapes
Section titled “Severity shapes”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-fillto decouple the two channels.
Hardware chrome
Section titled “Hardware chrome”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).
| Class | Part |
|---|---|
.cr-rivet · --hex · --slot | round rivet · hex bolt · slot screw |
.cr-screw · --x | slot screw · phillips (cross) screw |
.cr-bolt | square bolt head |
.cr-led · --wait/-done/-err/-idle | indicator LED, keyed to a signal (solid, no glow) |
.cr-vent · .cr-grille | louvred vent (horizontal) · grille (vertical) |
.cr-port | connector port |
.cr-stripe | hazard tape (--sig-wait diagonal) |
.cr-seam | panel seam (a hairline groove) |
.cr-plate | stamped ID plate (UNIT · CR-00 · REV.C) |
.cr-tally | tally-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.
Seeded chrome strip
Section titled “Seeded chrome strip”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=imgwith 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.
Richer textures
Section titled “Richer textures”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 Breach
Section titled “The Breach”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>| Class | Effect |
|---|---|
.cr-breach | soft corner + neon gradient rim + dual-hue glow (magenta → acid) |
.cr-breach--wash | tints the interior with the pair (kept dark; --ink stays legible) |
.cr-breach--alive | slow breathing dual glow (off under reduced motion) |
.cr-breach--work/-wait/-done/-err/-accent2 | re-key the primary hue to a signal |
.cr-blob | a 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
--aliveglow 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.mjs → lib/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.
Stepper
Section titled “Stepper”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).
Pin input
Section titled “Pin input”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).
A11y — role="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.
Tags input
Section titled “Tags input”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).
A11y — role="group" with a label; the entry is a labelled input; every
remove button names its tag (Remove <tag>).
Input group
Section titled “Input group”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).
Avatar
Section titled “Avatar”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).
Spinner
Section titled “Spinner”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).
Scroll area
Section titled “Scroll area”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).
Resizable
Section titled “Resizable”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.
ToggleChip
Section titled “ToggleChip”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.
Overflow
Section titled “Overflow”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?.
A11y — aria-expanded reflects state; aria-label = “show N more
RelativeTime
Section titled “RelativeTime”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.
Rating
Section titled “Rating”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.
Timeline
Section titled “Timeline”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.
Toolbar
Section titled “Toolbar”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). A11y — role="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.
File upload
Section titled “File upload”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.
Carousel
Section titled “Carousel”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.
Calendar
Section titled “Calendar”A month calendar grid — role="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.