@elizaos/ui owns the shared visual and interaction contracts for maintained
elizaOS frontends. The system has three layers.
- Semantic tokens in
src/stylesown color, typography, spacing, radius, elevation, motion, focus, dividers, and minimum touch targets. src/components/uiis the only atomic implementation layer. It owns native semantics, accessibility, interaction states, and base presentation.- Composites and domain adapters may arrange canonical atoms and add product behavior. They must not recreate the atom underneath them.
External consumers import stable exports such as @elizaos/ui/button or the
curated @elizaos/ui root. Code inside this package imports the owning module
directly. Consumers must not import Radix primitives, deep
@elizaos/ui/components/ui/* paths, or atomic variant helpers to restyle a
different element.
Canonical atoms expose typed variants for repeated visual states. A caller may
use className for its own placement and layout, including margins, width
constraints, flex placement, shrinking, and ordering. Color, background,
border, radius, typography, control height, padding, focus, hover, disabled,
selected, invalid, loading, and destructive presentation belong to the atom's
typed interface.
Message search and the command palette share the searchResult Button and
messageSearch Card recipes. Search controls use --radius-search so their
16px corners remain consistent in compact themes. The bg-bg-card-hover token
mixes the current card and strong-text colors into an opaque hover surface;
keyboard selection uses the same surface. data-surface="card" keeps chat
results opaque above the transcript, while ordinary list results rest on their
parent surface.
Raw semantic utilities can still reproduce a canonical recipe without using
its owner. The compliance gate therefore normalizes class order and rejects the
opaque padded surface recipe (bg-card, rounded-sm, and p-4) on raw hosts;
use Card variant="flatPadded". The corresponding outlined recipe is owned by
Card variant="outlinedPadded" and is independently order-normalized and
ratcheted to zero. Analytics-style report surfaces use
Card variant="reportPanel"; repainting Card with that background and border
pair is also rejected even when it arrives through an internal barrel. Card
also owns repeated vertical content spacing through
the typed stack axis and repeated column geometry through flow, so callers
do not repaint or redensify the surface through className.
Inset rows and compact settings panels use insetPadded or insetCompact.
Their shared bg-surface/border/radius recipe is normalized independently of
the density utilities, and repeated row alignment comes from the flow axis.
The same ownership rule applies to computed classes and React style objects.
Conditional expressions, templates, named class constants, CSS-module lookups,
and styling helper calls cannot conceal atom paint. Canonical controls reject
inline background, color, border, radius, shadow, outline, padding, height,
gap, fill, and stroke properties. Put dynamic placement on a non-control
wrapper. Use a reviewed exception when runtime domain paint cannot be expressed
as a finite typed presentation.
Button unstyled is migration debt, not a supported customization surface.
The compliance inventory counts every maintained use so the escape hatch can
only shrink. Replace a repeated presentation with a typed canonical variant;
do not rename the bypass or reproduce it in an adapter.
A local adapter is valid when it composes a canonical atom and owns meaningful
domain behavior. An adapter that only renames props or duplicates styling is a
violation. Add a canonical variant only after at least two maintained callers
demonstrate the same need. The compliance gate derives every Button variant,
size, shape, and alignment value from the canonical buttonVariants config and
counts its maintained Button and buttonVariants(...) call sites. A value
with fewer than two call sites fails. Stateful product presentation belongs in
an exported adapter that renders the canonical atom and is registered in
scripts/design-system-adapters.json. Each registry entry fixes the owning
file, exported symbol, canonical primitive, owner, rationale, and composition
count, and token role. The gate rejects unknown primitives, missing exports, moved or removed
compositions, count drift, and attempts to reuse an adapter's local recipe from
another caller. Exceptions remain reserved for renderer, native, or external
system boundaries; they do not waive the Button reuse rule.
scripts/component-inventory-decisions.json classifies the current atomic
inventory. scripts/atomic-consolidation-ledger.json separately records every
component accepted as consolidation debt and its resolution. An unresolved
entry remains an atomic-duplicate finding after deletion, rename, or decision
relabeling. A deleted resolution stays valid only while the old component and
any retired module remain absent and the canonical owner remains present. An
adapter resolution must keep composing that exact canonical owner. The
ordinary baseline may only hold or reduce the count. Completion uses the tight
zero baseline.
Canonical recipes are fail-closed. Every cva helper in components/ui, each
of its axes, and every registered adapter has an exact role in the compliance
gate. Adding a helper, axis, or adapter without a known role fails. The roles
admit semantic token families, not individual raw colors:
| Role | Foreground | Surface | Border/divider | Status and action | Radius | Spacing/density | Elevation | State |
|---|---|---|---|---|---|---|---|---|
action |
content, muted, on-action, inverse, contextual | neutral, inverse, transparent | structural, inverse, transparent | action and status families | control, container, pill | control scale | none or low | hover, focus, active, disabled, selected, invalid, pointer, responsive, group |
field |
content, muted, inverse | neutral, inverse, transparent | structural, inverse, transparent | status feedback only | control, container, pill | control scale | none | hover, focus, disabled, invalid, placeholder, file, pointer, responsive |
surface |
content, muted, inverse | neutral, inverse, transparent | structural, inverse, transparent | status feedback only | control, container, pill | container scale | semantic scale | hover, focus, active, disabled, selected, invalid, responsive, group |
status |
content, muted, on-action | neutral, transparent | structural, transparent | action and status families | control, container, pill | compact or container scale | none or low | hover, focus, active, disabled, selected, responsive, group |
content |
content, muted, action, status, contextual | transparent only | structural or transparent | foreground only | none | compact scale | none | hover, responsive, group |
layout |
none | none | none | none | none | layout scale | none | responsive or group-derived layout |
Semantic namespaces are txt/foreground content, bg/card/surface
neutral surfaces, border/input/ring structure, accent/primary action,
named status tokens, contextual header/sidebar tokens, and the stable
inverse pair for light-on-dark or light-on-accent controls. Palette utilities,
arbitrary color values, arbitrary control density, arbitrary elevation, and
transition-all are invalid in canonical and registered-adapter recipes.
The gate also normalizes semantic aliases when comparing variants inside an axis. Exact duplicates and recipes that differ only by equivalent aliases fail; one recipe must own that presentation. Layout differences do not make two paint recipes divergent, and distinct status tones remain distinct recipes.
Skeleton geometry is caller-owned because it previews the dimensions and shape of caller content. Skeleton paint, animation, and effects remain owned by the canonical primitive.
Tabs container layout is caller-owned because the root participates in its page's flex and overflow structure. Tab paint and interaction presentation remain owned by the canonical primitive.
Native inputs are typed adapters. File inputs use nativeFileHidden or
nativeFileDisplayNone, range inputs use nativeRange, and color inputs use
nativeColor. A native presentation cannot be paired with another input type.
Stories and composition tests must include the visible trigger, label, value,
focus, disabled, keyboard, ref, and change behavior that applies to the adapter.
Use the narrowest shared lifecycle owner that fits:
SettingsRowand the typed settings-control rows own settings label, description, control placement, disabled state, and row geometry. Domain adapters may add agent registration or value mapping, but must compose these rows instead of painting another settings shell.ContentStateowns empty and loading presentation across panel, inset, surface, and workspace placements. Page-specific wrappers supply copy and behavior rather than rebuilding the state geometry.PagePanel.ContentRailowns centered content widths and the shared 16px to 24px responsive horizontal inset for routed views. Headers, scrolling, vertical safe areas, and chat-composer clearance stay with their narrower route or workspace lifecycle owner.ActionListRowowns a single row-level action with native button, link, or static semantics and typed leading, copy, metadata, and trailing slots. It is not suitable for rows with multiple independent controls or domain progress.AuthResultShellowns the full-page background, centered card, and content geometry for public authentication outcomes. Result pages supply only their state-specific icon, copy, and actions.ConnectionCapabilityTileowns the icon, title, and description hierarchy used by connector setup grids. Provider screens supply translated content and provider-specific icon paint.StatusBadgeowns status paint. Domain adapters map typed domain states to a canonical tone and label; they do not reproduce badge classes.
Data tables use the canonical table parts. Rows stay flat and use the primitive-owned one-pixel semantic bottom divider. Do not wrap ordinary rows in individual cards or add caller-owned row borders. Header, hover, selected, and density presentation stay in the table primitives.
Repeated multi-atom structures belong in a shared pattern only when they own the same behavior and state lifecycle. Dependency similarity alone is not a pattern.
The molecular review follows this enforced sequence:
- Discovery groups components by role and atomic dependencies. Each decision
records the exact
file:symbolmember IDs and a semantic fingerprint of every member's atomic dependencies and rendered tags. Membership or rendered-structure drift invalidates the decision even when the broad dependency signature stays the same. - Review records
distinct-domain-compositionsorshared-lifecycle-owneronly after ownership is settled.unresolvedandcanonicalizeare explicit working states. Both fail the completion gate. Acanonicalizedecision also names the canonical contract that will own the result. - Before broad caller migration, a new entry in
scripts/molecule-contracts.jsonmust name at least two maintained consumers from different files. It must also name a Storybook story and a behavioral test that each render the contracted owner through its resolved import, including aliases and barrels. The audit verifies the owner, required composition signals, consumers, story, and test. - Migrate the remaining callers, remove the non-final cluster decision, and
regenerate the report.
audit:molecular-inventory --checkderives JSON and Markdown from one in-memory report and rejects either artifact when stale.
Rendered stories and the app visual audit own geometry proof. Source-text class assertions are not a substitute for rendered behavior.
audit:design-contract-graph derives a typed ownership graph from the live
semantic-token stylesheet, every maintained React component symbol, the
canonical atom inventory, molecule contracts, and higher-order declarations.
Source discovery is authoritative: aliases, barrels, and local wrappers are
resolved through TypeScript symbols, atom dependencies are closed transitively,
and reuse across independent source domains infers molecule ownership even when
no registry entry names the component. Registries add semantic stability
contracts to discovered owners; omission from a registry cannot hide an owner.
The graph validates token aliases, canonical import identity, downward dependencies, real exported owners, raw capability ownership, and exact migration debt. Debt is keyed by a semantic fingerprint and match count, so removing one finding cannot make room for a different finding under the same aggregate count. Tight CI also rejects stale and expired debt.
The graph distinguishes canonical implementations from route instances.
Organism and page-shell declarations may name component owners and supported
layout kinds, but route IDs and resolved layout policy remain in the existing
route declarations and SurfaceManifest. PageLayoutManifest keeps content,
workspace, and immersive topology separate from header framing and renderer
isolation. This prevents the design graph from becoming another route registry.
Repository-wide raw painted and interactive hosts remain migration inventory. Inside a source-inferred molecule, however, a raw host that claims surface, border, radius, elevation, or interaction capability is an enforced finding. New claims fail immediately. Existing migration debt is exact and expiring; tight mode also fails after a cleanup until the ledger is reduced, preventing removed debt from becoming allowance for unrelated drift. Completion requires that molecular capability debt reach zero.
The story-coverage ratchet checks both the number of covered components and the
coverage ratio against scripts/stories-coverage-baseline.json. It also rejects
any component newly added to the missing-story set, so swapping coverage between
two components cannot hide a regression. Run node scripts/stories-coverage.mjs --check for the enforced check or audit:story-coverage for a report.
Story Gate renders the built Storybook catalog in Chromium. Console errors that
remain after the static-harness noise filters and serious or critical axe
violations fail directly; neither uses a per-story allowlist or baseline. A
component that requires live app context may be classified needs-runtime,
which moves its coverage to the full app audit rather than turning missing
runtime state into a false component failure.
Run bun run --cwd packages/ui audit:design-system to scan maintained package
and plugin React sources. The committed baseline blocks every category from
growing. A cleanup must lower the affected count with
audit:design-system:update-baseline in the same change; the command refuses
to raise any count. CI uses a tight-baseline mode, so a stale allowance also
fails after cleanup instead of silently preserving room for regressions.
Product source and Storybook examples are governed. Test files, test fixtures, generated output, and templates are excluded because they imitate host and failure boundaries rather than ship as maintained product UI.
Legitimate renderer, native, or external-system cases live in
scripts/design-system-exceptions.json. Each exception names one rule, file,
symbol, owner, reason, review date, expected match count, and exact source lines
when the exception covers a specific occurrence. Inline suppressions are not
supported. The gate fails when an exception expires, moves, changes count, or
no longer matches a finding.
Zero violations means every ratcheted count is zero and every remaining non-canonical implementation is a current, centrally reviewed exception.