Aros HIG/Vol. II/ComponentsComponents
Volume II

Components

Every element Aros is built from, drawn once and specified to the pixel. v1.1 rebuilds the specimens to match what the operator console and the customer portal actually ship — a component here and a component there are the same component. If they disagree, one of them is wrong, and it is usually the one that was drawn by hand.

2.0 · How to read this volume

Reading a spec

Every component below follows the same skeleton so you can scan it the same way each time.

AnatomyThe named parts and where padding lives
VariantsDistinct purposes — not just colours
StatesDefault · hover · focus · active · disabled · loading · error
SizingThe allowed sizes and their tokens, at dense (console) unless stated
SpecPadding, radius, type, border — all as tokens
MotionWhat animates, for how long, on what curve
In codeThe primitive that implements it in aros-frontend / aros-portal
MUSTUse the component as specified. Restyling a component inline ("just a bigger radius here") forks the system and is treated as a defect.
SHOULDWhen a surface needs something this volume lacks, add the component here first, then build it. A primitive that exists only in one route is a fork with a head start.
2.1 · Components

Buttons

Buttons trigger actions. Hierarchy is communicated by variant, never by making everything loud.

2.1.1 Anatomy

123
  1. Container — height 36 (dense) / 40, padding --s3×18px, radius --r-md.
  2. Leading icon — optional, 16px, inherits text colour, gap --s2.
  3. Label — Atkinson 600, sentence case, the verb of the outcome.

2.1.2 Variants

Link
VariantPurposeSurfaceRule
PrimaryThe one main actionp500 fill · on-accent ink · hover p400Max one per view / section
SecondaryAlternative actionstransparent · 1px --border-strong · hover border p500 + ink p400Any number
GhostLow-emphasis / toolbartransparent · hover --washIn dense UI, icon-only
DestructiveIrreversible actionstransparent · 1px danger/45 · danger-ink · hover danger-tintAlways confirm
LinkAn action that reads as textpositive-ink · underline on hoverInside prose only
MUSTShow one primary button per view. If everything is primary, nothing is.
NEVERUse a solid green primary button for a destructive action, and never a solid red one either. Destructive is the bordered danger variant; green means "good / go", and a red fill is an alarm, not an affordance.

2.1.3 States & sizes

default
hover
focus
active
loadinglabel stays in the DOM for screen readers; the spinner replaces it visually
disabledNo connector is configured — tracked on aros-frontend#61
sizes
icon-only
group
MUSTA disabled control that has a reason delivers it through DisabledReason: a focusable wrapper with a real tooltip. A disabled button cannot be hovered or focused, so a title on it is invisible to everyone — and to keyboard users it never existed.
MUSTIcon-only buttons carry an aria-label. A glyph with no accessible name is invisible to assistive tech.

2.1.4 Spec & motion

heightsm 32 · base 36 · lg 44 (dense) — sm 36 · base 40 · lg 44 (comfortable)
paddingsm 0 12 · base 0 18 · lg 0 22 · icon-only 36×36
radius--r-md (9)
typeAtkinson 600 · 14 (dense) / 16 · sm 13
icon16px, gap --s2
focus2px --p500 ring, 2px offset — the one focus grammar for every control
disabledopacity .4, pointer-events none
motionbg + translateY(1px) on press · d-quick · standard
in codeui/button.tsx (variants: default · outline · secondary · ghost · destructive · link)
2.2 · Components

Fields & controls

Inputs collect data. Every field has a visible label, a focus state with a green ring, and clear error messaging — delivered by one field grammar, so a form built next year matches one built today.

2.2.1 The field grammar

Not a valid hostname.123
  1. Label — always visible, always associated (for/id). "optional" is stated; "required" is the default and unmarked.
  2. Control — input, textarea or select on the page surface; aria-invalid + aria-describedby are wired by the grammar, not by hand.
  3. Hint or error — one line beneath. The error replaces the hint and is announced (role="alert").

2.2.2 Text input — states

default
FQDN or IPv4.
focus
error
Not a valid hostname.
disabled
search
/
/ focuses it from anywhere on the page

2.2.3 Select, checkbox, radio, toggle

Checkbox
Object lock Versioning
Radio
Raft DCS etcd DCS
Toggle
MUSTEvery field has a visible label. Placeholder text is a hint, never a substitute for a label — it vanishes on input.
SHOULDUse a toggle for an instant on/off state, a checkbox for a selection confirmed on submit. They are not interchangeable.

2.2.4 Spec

controlheight 36 · padding 0 12 · radius --r-md · 1px --border-strong on --bg
focus ringborder --p500 + 3px --p-tint glow · d-fast
errorborder --danger + help in --danger-ink, role="alert"
labelAtkinson 500 · 14 (dense) / 15 · --text · gap --s2 (6px) to the control
hint13 · --text-2 · replaced by the error
selectnative, appearance none, chevron drawn in --text-3
in codeui/field.tsx + ui/input.tsx (Input · Textarea · Select) — portal: PortalField on the same class
2.3 · Components

Status & badges

Aros is a status product. These are its most important components — and the most disciplined. State is always colour + shape + label, never colour alone.

2.3.1 The live dot

Healthy Degraded Offline Unknown

Only the healthy dot pulses — the signature heartbeat, meaning "live and observed right now." Degraded, offline, and unknown are static. Under prefers-reduced-motion the ring is removed and the dot stays.

2.3.2 Pills, badges, tags, severity

status pillsOperationalDegradedOfflinePending
explainedDegraded▾
One or more health signals are outside their threshold.
corosync_offline
cluster membership reports this node offline
Threshold: 2 consecutive fails · Evaluated: 2026-09-02 01:14:07
badgesEC:4v1.1drift3 cvedev
tagspve1aros-pg-dev-1role=app
severityP1P2P3P4

2.3.3 State → colour → glyph

StateFillInkDotWords we use
Healthy--positive--positive-inkpulsingOperational · Healthy · Live · Up
Degraded--warn--warn-inkstaticDegraded · Read-only · At risk · Running · Burning
Offline--danger--danger-inkstaticOffline · Down · Failed · Exhausted · Detected
Unknown--neutral--neutral-inkstaticUnknown · Pending · Queued · Paused · Not yet run
MUSTA status pill renders the human label, never the wire enum: "Not yet run", not never_run. When the surface knows why, the pill expands to say so — the meaning, the causes, the rule that produced it, when it was evaluated — and says "not reported" where it does not know, rather than staying quiet.
NEVERCommunicate status with colour alone. A red dot with no label is useless to a colour-blind operator and ambiguous to everyone at a glance.
pillPlex Mono 500 · 13 (dense) / 14 · radius pill · padding 2 10 2 8 · dot 8px · 1px tint border · tint fill · ink text
badgePlex Mono 500 · 13 · UPPER +0.12em · radius --r-sm · padding 2 8
tagPlex Mono · 13 · UPPER +0.08em · radius --r-xs · --surface-3 · 14px icon
severityPlex Mono 600 · 13 · UPPER · P1/P2 danger (P1 stronger tint) · P3 warn · P4 info
in codeui/status-badge.tsx (exhaustive over every wire status) · ui/badge.tsx · ui/tag-chip.tsx · ui/severity-badge.tsx
2.4 · Components

Cards, tiles & metrics

Cards group related information on a raised surface. Tiles are cards that carry one number. The metric strip is the hero ribbon — the instrument's dial.

pve1Healthy
187 GB RAM · 28 TB HDD-R5
Storage workhorse
pve2Healthy
251 GB RAM · local-lvm
Compute node
pve3Degraded
15 GB RAM · quorum-class
RAM upgrade queued

2.4.1 KPI tiles

Open findings
3 P2 1 P3 2
Pending approvals
1
Last discovery
4m ago
37 entities

2.4.2 Metric strip

3
Hosts
3 healthy · 3 PVE
104
Physical cores
55%
RAM used
496 / 896 GiB
card--surface on --bg · 1px --border · radius --r-lg (14) · inset --s6 · no shadow
card titleAtkinson 600 · 16 · +0.01em
hover (interactive only)border --border-strong + translateY(-2px) · d-quick · standard
kpi tileeyebrow + 32px value (tabular) + one line of context or one link
metric stripPlex Mono 500 30 · eyebrow · hairline separators · right-aligned · tone tints the value only, via ink
in codeui/card.tsx · ui/metric-strip.tsx · ui/telemetry-tile.tsx
SHOULDOnly make a card interactive (hover lift) if the whole card is a single link/target. A card full of separate buttons must not also lift.
NEVERNest a larger radius inside a smaller one, or put a shadow on a flat card. Depth is tint + hairline; shadows are for layers that float (§2.8).
2.5 · Components

Tables

Tables are how Aros shows the estate. Dense, scannable, monospaced where it counts — and since v1.1, selectable, exportable, and pinned.

2 selected
ResourceHostTypeStatusUptimeRAM
pve1 · 2 items
aros-pg-dev-1pve1VM 231Live42d 6h16 GiB
aros-minio-4pve1VM 274SPOF18d 2h8 GiB
pve2 · 1 item
aros-pg-dev-2pve2VM 232Live42d 6h16 GiB
Page 1 of 4 · 87 total
headereyebrow (Plex Mono 13 · +0.14em · UPPER · --text-3) · 1px --border-strong below · sticky when the table scrolls
cell padding12 × 14 (compact 8 × 12)
row divider1px --row-divider · hover --wash · d-fast
selection16px checkbox, accent p500 · selected row --p-tint · header checkbox is tri-state
bulk barappears only while rows are selected · --p-tint · count in mono · actions are secondary buttons · a blocked action carries its DisabledReason
group roweyebrow on --wash · "key · n items"
paginationPlex Mono 13 · "Page n of m · total" left · Previous/Next right
emptyone row, centred, --text-2, names the entity and the reason (§2.13)
in codeui/data-table.tsx (+ selection, bulkActions, csv, views, groupBy, stickyHeader)
MUSTRight-align and use tabular figures for numeric columns (uptime, counts, sizes). Left-aligned proportional numbers can't be compared down a column.
MUSTEvery row that opens something has a real link or control inside it. A whole-row click is additive chrome; keyboard access never depends on it.
SHOULDLead each row with a 16px domain icon for its resource type. The icon does the scanning the eye wants to do. Offer the row's actions on right-click too (§2.8.4) — the menu button is the discoverable path, the context menu is the fast one.
NEVERShrink type to fit more rows. Density is a padding decision; 13px is still the floor at compact.
2.7 · Components

Feedback

Telling the operator what happened. Severity maps to the semantic palette, every time — and every outcome is reported exactly once, on exactly one channel.

Migration 0012 applied. notification.alert and remediation_candidate tables created on dev.
Discovery cycle running. 2 entities touched · next sweep in 5 min.
MinIO SPOF. aros-minio-1 and aros-minio-4 are both on pve1.
Quorum lost. pve3 unreachable — cluster is read-only.

2.7.1 Toast

Done
Credential rotated on r740xd.
Awaiting approval
Tenant create is parked. Track it under Approvals.
Failed
Couldn't reach pve3 — connection timed out after 30s.
ChannelOwnsShape
alertA condition you need to know about, in placePersistent, inline. Tinted bg + 1px border in the semantic colour, icon in the ink.
toastThe outcome of a write whose surface is gone (dialog closed, row removed)--surface-3 · 3px tone edge · eyebrow label in the ink · bottom-right of the content well · z-toast · success auto-dismisses at 6s; deferred and failed persist until dismissed
bannerThe whole system is affectedFull-width alert at the top of the well
inline noticeA write whose surface survives (a form still to correct)role="alert" beside the field; deferred is a neutral pill with the approval link
MUSTReport each outcome once. A write toasts iff no surviving surface reports it; reads never toast; a render failure is a boundary, never a toast. Toasts are announced politely — never role="alert".
SHOULDUse a toast for "it worked," an alert for "here's a condition you need to know about," and a banner for "the whole system is affected."
2.8 · Components

Overlays

Layers above the page: modals for decisions, side panels for routine forms, popovers for menus, tooltips for labels. Every one traps focus, closes on Esc, and hands focus back.

2.8.1 · Modal
Destroy VM 109?
monitoring.ryanit.com will be repointed first. This action is permanent.
2.8.2 · Side panel
Register connector
The API base URL.
2.8.3 · Popover menu
aros-pg-dev-1
Open↵
Copy ID⌘C
Rescan
Delete
2.8.4 · Context menu

Right-click on a row, a card or an identifier opens the same menu the row's ⋯ button opens — same items, same order, same keycaps. Never an action that exists only on right-click.

Open in new tab
Copy hostname
Show in inventory
2.8.5 · Tooltip
aros-pg-dev-3 · leader · 10.0.0.12 Rotates the credential on the BMC and re-verifies it.
No connector — tracked on aros-frontend#61
modal--surface-2 · 1px --border-strong · radius --r-lg · shadow-lg · max 512px · scrim --scrim + blur · footer on --wash behind a divider · primary bottom-right
side panelsame surface, anchored right, 360–480px · for routine create/edit forms and detail drawers
popover / context menu--surface-3 · 1px --border-strong · radius --r-md · shadow-md · items 14 · icon 16 · keycaps right · destructive last, after a divider, in --danger-ink
tooltip--tip-bg (obsidian) · 13 · radius --r-sm · 300ms hover delay, no delay on focus · mono for identifiers · never a control inside
zmenus z-modal · tooltip z-toast (above everything, it is transient)
in codeui/dialog.tsx · ui/dropdown-menu.tsx · ui/context-menu.tsx · ui/tooltip.tsx (Tooltip, DisabledReason)
MUSTA modal traps focus, closes on Esc and on scrim click, and returns focus to its trigger. The scrim uses z-modal with a darkened backdrop.
MUSTA tooltip opens on focus as well as hover, is attached with aria-describedby, and holds text only. If it needs a button, it is a popover.
SHOULDReserve modals for decisions that must interrupt. A routine form belongs in a side panel; a destructive confirmation names its object ("Destroy VM 109", not "Delete").
2.9 · Components

Command palette

One box that finds anything and goes anywhere. It is the keyboard's front door — and since v1.1 it has a visible one in the top bar, so nobody has to be told about ⌘K.

Search or jump to…⌘K
monitoringesc
Knowledge
Edge Traefik down — runbook runbookedge-traefik-down · ↵
Go to
Monitoringchecks + services · g m
Monitoring › Servicesservice registry
Actions
New incidentn i
Switch themeObsidian → Paper
open⌘K / Ctrl+K anywhere · the top-bar front door · / is the page's local search, not the palette
groupsKnowledge (live full-text) · Go to (every rail + sub-nav destination, derived from the nav model, never a second list) · Actions (the view's verbs, plus global ones)
rowicon 16 · label · hint right in --text-3 · the highlighted row shows ↵ · ↑↓ move, ↵ activates, Tab reaches every row, Esc closes
surface--surface-3 · 1px --border-strong · radius --r-lg · shadow-lg · 12vh from the top · scrim + blur
in codelayout/CommandPalette.tsx
MUSTThe palette is a real dialog: aria-modal, Tab contained, focus returned to the opener on close, and exclusive with every other modal surface — opening it closes the drawer, and vice versa.
SHOULDShow the keyboard sequence beside a destination ("g m") so the palette teaches the shortcuts that make it unnecessary.
2.10 · Components

Keyboard & shortcuts

An instrument is operated, not browsed. The console is fully usable from the keyboard, and the fast paths are learnable because every one of them is written down — in a tooltip, a menu keycap, or the help sheet.

keycap⌘Kesc↵gthend
Command palette⌘K
Focus the page search/
Shortcut help?
Close · dismiss · clearesc
Go to Dashboard · Incidents · Monitoringgd · gi · gm
Next / previous rowj / k
Open the highlighted row↵
Toggle theme⌘⇧L
keycapPlex Mono 500 13 · 20px tall · radius 4 · 1px --border-strong on --wash-2 · chords as separate caps with a 4px gap
grammar⌘/Ctrl chords for global surfaces · single keys only outside editable fields · "g then x" sequences for navigation, 1s window · j/k for lists
discoveryevery shortcut appears at least twice: in the control's tooltip or menu keycap, and on the ? help sheet
in codeui/kbd.tsx · lib/shortcuts.ts
NEVERSteal a key from typing. A single-key shortcut is ignored while focus is in an input, textarea, select or contenteditable, and a sequence never begins there.
MUSTEverything a pointer can do, a keyboard can do — including reaching a disabled control's reason. Hover-only actions are an accessibility failure, not a design choice.
2.12 · Components

Code & terminal

Aros shows commands and output verbatim. Monospace, syntax-tinted, copyable — and on obsidian in both themes.

Terminal
hlvm@iaccn01:~$ vault kv get secret/aros/grafana/postgres
==== Data ====
db_name aros_grafana
username grafana
password <hidden>
Config block
patroni:
  dcs: raft     # DCS-agnostic toggle
  nodes: 3      # one per physical host
MUSTCode and command output are always Plex Mono on the darkest surface (--surface-code). Never paraphrase a command into prose.
timestampsrelative for scanning ("4m ago"), absolute for the record (2026-09-02 · 14:03:07), always with the UTC instant one hover away and a <time datetime> in the DOM; past a week the relative form becomes the date
identifiersPlex Mono · click copies · "Copy" → "Copied" · a truncated value keeps its full form in the tooltip
in codeui/when.tsx + lib/time.ts · ui/copyable.tsx
SHOULDMake every identifier copyable in one click — a hostname, an ID, a hash, an address — with the full value in a mono tooltip when it is truncated.
MUSTA timestamp without a zone is ambiguous evidence. Every rendered time carries its UTC instant, reachable by hover and by assistive tech.
2.13 · Components

Empty · loading · error

The states most products forget. Aros treats them as first-class, because an operator sees them at the worst moments.

No resources yet

Discovery hasn't found anything on this host.

Shimmer, not a spinner. Resolves into data, empty or error — never hangs.

Couldn't reach pve3

Connection timed out after 30s.

Retry is offered only when retrying can help — never on a 403.

empty40px glyph disc on --surface-2 · headline 14/600 that names the ENTITY · one sentence that names the REASON · one next action (or a disabled one with its reason)
skeleton--surface-3 blocks · --wash-2 sweep · 1400ms linear · static under reduced-motion · table rows keep the header visible
errordanger tint + 1px danger/30 · icon in --danger-ink · what happened · Retry when it can help
partialwhen some reads answered and some did not, say so above the data: "2 of 5 reads did not answer — this page is incomplete, not quiet"
in codeui/empty.tsx · ui/loading.tsx · ui/error-panel.tsx · ui/query-boundary.tsx · ui/unavailable-reads-notice.tsx
MUSTEvery empty and error state names what happened and offers one next action. A blank panel or a bare spinner with no context is a defect.
NEVERShow a skeleton forever, or render a failed read as an empty one. "No hosts have an open CVE" is a security verdict; a page with no evidence for it hides the list and says why.

2.13.1 Promised surfaces — the four tiers of an honest skeleton

A page may be drawn before every part of it works, so the estate can be restructured for the long term without removing what works today. The rule that keeps it honest: a skeleton always says which tier it is in, and the first two tiers link the issue that brings them to life.

Not built yet · frontend
Pending updates

Every pending update across the estate, in one train.

aros-frontend#203
Waiting on the backend
Backup jobs

PBS job inventory, missed-backup findings, trigger and restore.

aros-backend#139
Should work · failing
Host telemetry

Proxmox connector unreachable.

Loading
frontenddesigned, data exists, not yet built · info ink · dashed frame · links the frontend issue
backenddesigned, the backend it needs is not there · warn ink · dashed frame · links the backend issue
failingeverything exists and this read is failing now · danger ink + the message · Retry when it can help
loadingthe ordinary shimmer — no badge, no frame; must resolve
silhouettethe designed layout at 50% opacity, aria-hidden and inert — skeleton blocks or real components with empty props, never sample values
in codeui/promised.tsx (Promised, PromisedSilhouette) · lib/forgejo.ts
MUSTA promised surface names its tier and, for the first two tiers, the issue that owns it. A silhouette with no badge is a lie; a badge with no issue is a wish.
NEVERPut a number, a name or a status in a silhouette. The moment a placeholder looks like data, someone will read it as data.
2.14 · Components

Avatars & progress

RT
AK
RT
AK
+3

2.14.1 Progress & meters

EC overhead · 33%
pve1 storage · 66%
pve3 memory · 94%
SHOULDMeters change colour by threshold — green under 70, amber to 90, red above — so the bar's colour carries the warning, not just its length. Absent data is a dashed track, never a fabricated full bar.
End of Volume II

Next: Patterns

Components are the vocabulary. Volume III — Patterns → is the grammar: how they compose into the shell, the dashboard, and the flows.

Copied