AkurAI Build
Menu

Public repository / 439f0dcabf5cdf2b120c7724be895cf1df30507f

AkurAI-Framework / site/content/docs/design-system/components.md

Latest change b14b0a0ca75df1c2b07ac75b288f7085176ee32b - OLI-158: declarative [[forms]] handler (validate, store, SMTP notify, PRG, honeypot, rate limit, i18n) + glass form classes by Ólafur Búi Ólafsson

# Component catalog

Every component the AkurAI design system ships or intends to ship, grouped by
job. Each entry names its **intent**, **variants**, **states**, an example of the
**markup** (semantic classes, no framework runtime), **accessibility**
requirements, and the **tokens** it consumes. This is the build spec for the
`crates/css` engine and the frontend layer: a component is "done" when its
markup, states, and a11y here all hold.

> **Status** — ✅ shipped · 🟡 partial · ⬜ planned. Naming convention:
> `block` + `block--modifier` style, but written as separate classes today
> (`btn btn-primary`) to match the existing CSS. Every component is plain
> server-renderable HTML; interactivity prefers native elements
> (`<details>`, `<dialog>`, `:focus-within`) over scripted behaviour.

---

## Actions

### Button ✅

Trigger an action or navigation. The most-used control; gets the most states.

- **Variants** — `btn-primary` (gradient, brand), `btn-secondary` (panel),
  `btn-ghost` (transparent), `btn-danger` ⬜ (destructive), `btn-link` ⬜
  (text-only). Sizes: `btn-sm` ✅, default, `btn-lg` ⬜.
- **States** — default, `:hover`, `:active` (1px press), `:focus-visible`
  (accent ring), `[disabled]`, `is-loading` ⬜ (spinner + label hidden).
- **Markup**

```preview
<a class="btn btn-primary" href="/start">Get started</a>
<button class="btn btn-secondary btn-sm">Save</button>
<button class="btn btn-secondary" disabled>Unavailable</button>
```

- **A11y** — use real `<button>` for actions, `<a>` for navigation; never a
  clickable `<div>`. Focus ring is `:focus-visible` only. A loading button keeps
  its width and sets `aria-busy="true"`.
- **Tokens** — `--accent`, `--panel-2`, `--border-2`, `--radius-btn`, focus glow.

### Button group ⬜

Join related buttons into one segmented control (e.g. view switch).

- **Variants** — `btn-group` (attached), `btn-group-spaced`.
- **States** — one child may be `is-active` (pressed look).
- **Markup**

```preview
<div class="btn-group" role="group" aria-label="View">
  <button class="btn btn-secondary is-active">List</button>
  <button class="btn btn-secondary">Grid</button>
</div>
```

- **A11y** — wrap in `role="group"` with a label; the active item carries
  `aria-pressed="true"`.

### Icon button ⬜

A square button with only an icon — toolbars, table-row actions.

- **Variants** — `btn-icon`, `btn-icon btn-ghost`, sizes sm/md.
- **A11y** — **mandatory** `aria-label` (no visible text); icon is
  `aria-hidden="true"`.
- **Tokens** — `--radius-btn`, `--muted` (idle), `--fg` (hover).

### Dropdown menu ⬜

A button that reveals a list of actions or links.

- **Anatomy** — trigger button → floating `menu` panel → `menu-item`s,
  `menu-separator`, optional `menu-label`.
- **States** — `open`, item `:hover`/`:focus`, `is-disabled`, `is-destructive`.
- **Markup**

```preview
<details class="dropdown">
  <summary class="btn btn-secondary">Actions ▾</summary>
  <div class="menu" role="menu">
    <a class="menu-item" role="menuitem" href="#">Edit</a>
    <button class="menu-item is-destructive" role="menuitem">Delete</button>
  </div>
</details>
```

- **A11y** — native `<details>` gives open/close + Esc for free; for full
  arrow-key roving focus, enhance progressively. `role="menu"` /
  `role="menuitem"` on the panel.
- **Tokens** — `--panel`, `--border`, `--shadow`, `--z-dropdown`.

### Link ✅

Inline navigation. Accent colour, underline on hover.

- **Variants** — default, `link-muted` ⬜, `link-external` ⬜ (trailing ↗).
- **A11y** — descriptive text (never "click here"); external links get
  `rel="noopener"` and an announced "(opens in new tab)" when targeted.

---

## Forms & inputs

### Field ✅

The wrapper that pairs a control with its label, hint, and error — the unit
forms are actually built from.

- **Anatomy** — `field` → `<label>` + control + `field-hint` + `field-error`.
- **States** — default, `is-invalid` (danger border + visible error),
  `is-disabled`, `is-required` (mark on label).
- **Markup**

```preview
<div class="field is-invalid">
  <label for="email">Email</label>
  <input id="email" type="email" aria-describedby="email-err" />
  <p class="field-error" id="email-err">Enter a valid address.</p>
</div>
```

- **Server-driven** — declarative `[[forms]]` re-render with `form.errors` /
  `form.values` (see [Forms](/docs/forms)); the honeypot input uses `.hp`.
- **A11y** — `<label for>` ties to the control; errors link via
  `aria-describedby` and set `aria-invalid="true"`; required is real `required`
  plus a visual mark.

### Text input ✅

Single-line text. Base for email, password, search, number, url, tel.

- **Variants** — `text`/`email`/`password`/`number`/`url`; sizes sm/md;
  `input-group` ⬜ with prefix/suffix add-ons.
- **States** — default, `:focus` (accent ring), `:disabled`, `[readonly]`,
  `is-invalid`, with-placeholder.
- **Markup**

```preview
<input type="email" placeholder="ada@example.com" />
```

- **A11y** — always labelled (visible `<label>` or `aria-label`); placeholder is
  never the label. `:focus` ring via box-shadow.
- **Tokens** — `--bg`, `--border`, `--accent`, `--radius-sm`.

### Textarea ✅

Multi-line text; vertical resize only.

- **States** — same as text input; `rows` sets initial height.

### Select ✅

Native single choice. Styled to match inputs.

- **Variants** — `select`, `select-sm`; multi-select ⬜.
- **A11y** — keep the native `<select>` for keyboard + screen-reader support;
  only restyle the box, never replace with a div-listbox unless the Combobox is
  used.

### Combobox / Autocomplete ⬜

Type-to-filter select over many options; the bridge to search.

- **Anatomy** — input → `listbox` popup → `option`s, with an empty state.
- **States** — open, filtering, active-descendant highlighted, no-results,
  loading (async).
- **A11y** — `role="combobox"` + `aria-expanded` + `aria-activedescendant`;
  arrow keys move the active option, Enter selects, Esc closes.

### Checkbox ✅

Independent boolean(s).

- **States** — unchecked, checked, `indeterminate` ⬜ (parent of a partial set),
  disabled, focus ring.
- **Markup**

```preview
<label class="inline"><input type="checkbox" checked /> Subscribe</label>
```

- **A11y** — native input; indeterminate set via the DOM property and
  `aria-checked="mixed"`. Tokens: `accent-color: var(--accent)`.

### Radio ✅

One choice from a small set.

- **A11y** — group in a `<fieldset>` with `<legend>`; arrow keys move within the
  group (native behaviour).

### Switch / Toggle ⬜

Instant on/off for settings (no Save needed).

- **States** — off, on, disabled, focus.
- **Markup**

```preview
<label class="switch">
  <input type="checkbox" role="switch" />
  <span class="switch-track"></span> Email notifications
</label>
```

- **A11y** — it is a checkbox with `role="switch"`; label describes the setting,
  not the state. Distinguish from checkbox: a switch applies immediately.

### Range / Slider ✅

Pick a number along a continuum.

- **Variants** — single value ✅, dual-handle range ⬜, with tick marks ⬜.
- **A11y** — native `<input type="range">`; pair with a visible value readout.

### File upload ⬜

Select or drop files. Maps to the framework's upload endpoint.

- **Anatomy** — drop zone → hidden file input → selected-file list with progress
  + remove.
- **States** — idle, `is-dragover`, uploading (per-file progress), success,
  error (too big / wrong type).
- **A11y** — a real `<input type="file">` behind the styled zone; drag-drop is an
  enhancement, never the only path. Announce upload status via a live region.

### Date / time picker ⬜

Pick a date, time, or range.

- **Variants** — date, datetime, range; native-first.
- **A11y** — start from `<input type="date">`; a custom calendar must support
  full keyboard grid navigation and announce the focused day.

### Search input ⬜

Text input specialised for search: leading icon, clear button, `type="search"`.

- **States** — empty, typing, with-clear, loading, with-results-count.

### Form layout ⬜

Arrange fields: `form-grid` (responsive columns, `span-2` to span) ✅ for
demos, plus `form-section` ✅ (titled `<fieldset>` group) and `form-actions` ✅
(footer button row, right-aligned). `form-error` ✅ shows a form-level message.

---

## Data display

### Card ✅

A grouped surface for one entity or summary.

- **Variants** — `card` ✅, `card-interactive` ⬜ (hover lift, whole-card link),
  `card-media` ⬜ (image header). Slots: title, body, `card-footer` ⬜.
- **Markup**

```preview
<div class="card">
  <h3>Crash-safe by design</h3>
  <p class="muted">Atomic copy-on-write commits.</p>
</div>
```

- **A11y** — a clickable card needs one real link; don't nest interactive
  elements inside a card-wide link.
- **Tokens** — `--panel-2`, `--bg-2`, `--border`, `--radius`.

### Table ✅

Tabular rows. The workhorse of the admin UI.

- **Variants** — `table` ✅, `table-striped` ⬜, `table-compact` ⬜,
  `table-sticky-head` ⬜.
- **Features (admin) ⬜** — sortable headers, row selection (checkbox column),
  row hover, inline actions, sticky first column, resize.
- **States** — hover row, selected row, sorted column (asc/desc), loading
  (skeleton rows), empty (empty-state slot).
- **Markup**

```preview
<table class="table">
  <thead><tr><th>Id</th><th>Title</th></tr></thead>
  <tbody><tr><td>01</td><td>Hello</td></tr></tbody>
</table>
```

- **A11y** — real `<table>`/`<th scope>`; sortable headers are `<button>`s
  inside `<th>` with `aria-sort`. Selection column header has a label.
- **Tokens** — `--border`, `--muted` (head), `--panel-2` (hover/selected).

### Description list ⬜

Key/value detail view for a single record.

- **Markup**

```preview
<dl class="desc-list">
  <dt>Status</dt><dd><span class="badge badge-ok">Active</span></dd>
  <dt>Created</dt><dd>2026-06-25</dd>
</dl>
```

### Stat / Metric ✅

A single headline number with a label.

- **Variants** — `stat` ✅, with delta/trend ⬜ (▲ green / ▼ red), with sparkline
  ⬜.
- **Markup**

```preview
<div class="stat"><div class="stat-value">~660 KB</div><div class="stat-label">Binary size</div></div>
```

### Badge ✅

Tiny status pill — non-interactive.

- **Variants** — `badge-ok` ✅, `badge-muted` ✅, `badge-warn`/`badge-info`/
  `badge-danger` ⬜, `badge-dot` ⬜ (leading status dot).
- **A11y** — colour is never the only signal; the text carries the meaning.

### Tag / Chip ⬜

Like a badge but interactive — removable filters, multi-select tokens.

- **Variants** — static, removable (× button), selectable.
- **A11y** — the remove control is a real `<button>` with
  `aria-label="Remove X"`.

### Avatar ⬜

Represent a user or entity.

- **Variants** — image, initials fallback, sizes xs–lg, `avatar-group`
  (overlapped stack with +N overflow), status-dot overlay.
- **A11y** — `alt` is the person's name; decorative grouping is `aria-hidden`.

### Code block ✅

Monospace, scrollable code. Used in docs and the install snippet.

- **Variants** — `code-sample` / `install` ✅, with-filename header ⬜,
  copy-button ⬜, line-numbers ⬜.
- **Tokens** — `--mono`, `--border`, dark code surface.

### Timeline ⬜

Ordered events down a vertical rail — audit log, record history.

### Tree view ⬜

Nested, expandable hierarchy — collection/field navigation.

- **A11y** — `role="tree"`/`treeitem`/`group` with `aria-expanded`; arrow keys
  expand/collapse and move.

---

## Feedback

### Alert / Callout ✅

A persistent, inline message tied to a region of the page.

- **Variants** — `alert-success` ✅, `alert-info` ✅, `alert-warning` ✅,
  `alert-danger` ✅; `alert-dismissible` ⬜ (close button), with-icon ⬜,
  with-action ⬜.
- **Markup**

```preview
<div class="alert alert-info"><strong>Note.</strong> <span>TLS terminates at the edge.</span></div>
```

- **A11y** — error/success that appears after an action uses `role="alert"`
  (assertive); a static informational note does not.
- **Tokens** — semantic colour tinted into `--panel` via `color-mix`.

### Toast / Notification ⬜

A transient, floating message after an action (saved, copied, failed).

- **Variants** — success/info/warning/danger; with action ("Undo"); auto-dismiss
  vs sticky.
- **Anatomy** — a `toast-region` (fixed corner) stacks `toast` items.
- **States** — entering, visible, leaving, paused-on-hover.
- **A11y** — the region is an `aria-live="polite"` container (assertive for
  errors); never steal focus. Respect `prefers-reduced-motion`.
- **Tokens** — `--panel`, `--shadow`, `--z-toast`, semantic accent.

### Banner ⬜

A full-width, page-level message (system status, trial expiring).

- **Variants** — info/warning/danger; dismissible; sticky-top.

### Progress ⬜

Show completion of a known-length task.

- **Variants** — `progress` (bar), `progress-indeterminate`, `progress-circular`.
- **A11y** — `role="progressbar"` + `aria-valuenow/min/max`.

### Spinner / Loader ⬜

Indeterminate busy indicator for short waits.

- **Variants** — sizes xs–lg; inline (in a button) vs centered (in a panel).
- **A11y** — `role="status"` + visually-hidden "Loading"; CSS-only animation.

### Skeleton ⬜

Greyed placeholders that mimic content shape while loading — preferred over
spinners for tables, cards, and lists.

- **Variants** — text line, avatar, card, table-row; shimmer animation.
- **A11y** — container `aria-busy="true"`; skeletons are `aria-hidden`.

### Empty state ⬜

What a list/table/search shows when there is nothing — first-run, no-results,
error.

- **Anatomy** — icon → heading → one-line explanation → primary action.
- **Markup**

```preview
<div class="empty">
  <h3>No records yet</h3>
  <p class="muted">Create your first record to get started.</p>
  <a class="btn btn-primary">New record</a>
</div>
```

---

## Navigation

### Navbar ✅

Top, sticky, blurred app/site bar: brand + links.

- **Variants** — site nav ✅, app nav (with user menu) ⬜, with mobile toggle ⬜.
- **A11y** — wrap links in `<nav aria-label>`; mark the current link
  `aria-current="page"`.
- **Tokens** — `--bg` (translucent), `--border`, `backdrop-filter`, `--z-sticky`.

### Sidebar nav ✅

Vertical, grouped navigation — docs and the admin shell.

- **Variants** — `sidebar` with `sidebar-group` + `sidebar-title` ✅; collapsible
  groups ⬜; active-item highlight ⬜ (drive off the `active` slug).
- **A11y** — `<nav aria-label>`; current item `aria-current="page"`.

### Tabs ⬜

Switch between sibling panels in place.

- **Variants** — underline, pill, vertical.
- **States** — active, hover, disabled, focus.
- **A11y** — `role="tablist"`/`tab`/`tabpanel`; arrow keys move between tabs,
  `aria-selected` tracks the active one.

### Breadcrumbs ⬜

Show position in a hierarchy.

- **Markup**

```preview
<nav class="breadcrumbs" aria-label="Breadcrumb">
  <a href="/">Home</a> <span aria-hidden="true">/</span>
  <a href="/posts">Posts</a> <span aria-hidden="true">/</span>
  <span aria-current="page">Edit</span>
</nav>
```

### Pagination ⬜

Page through a long result set — paired with the data table.

- **Variants** — numbered, prev/next only, with page-size select, with
  result-count.
- **A11y** — `<nav aria-label="Pagination">`; current page `aria-current="page"`;
  disabled ends are not focusable.

### Steps / Stepper ⬜

Show progress through a multi-step flow (wizard, onboarding).

- **Variants** — horizontal, vertical; states per step: done, current, upcoming.

### Command palette ⬜

Keyboard-first action/search overlay (⌘K).

- **Anatomy** — overlay → search input → grouped results → keyboard hint footer.
- **A11y** — a Combobox in a modal: traps focus, `aria-activedescendant`,
  Esc closes, restores focus to the trigger.

### Anchor / table of contents ⬜

In-page heading nav with scroll-spy for long docs.

---

## Overlays

All overlays share one rule: **trap focus while open, restore it on close,
close on Esc, and label the dialog**. Prefer the native `<dialog>` element.

### Modal / Dialog ⬜

Focused task or confirmation over a dimmed page.

- **Variants** — sizes sm/md/lg; `dialog-confirm` (title + message + 2 buttons);
  `dialog-form` (a form inside).
- **Anatomy** — backdrop → `dialog` panel → header (title + close) → body →
  footer actions.
- **States** — opening, open, closing; `is-busy` while submitting.
- **Markup**

```preview
<dialog class="modal">
  <h2>Delete record?</h2>
  <p class="muted">This cannot be undone.</p>
  <div class="form-actions">
    <button class="btn btn-secondary" value="cancel">Cancel</button>
    <button class="btn btn-danger" value="confirm">Delete</button>
  </div>
</dialog>
```

- **A11y** — native `<dialog>` gives focus-trap, Esc, and backdrop; set
  `aria-labelledby` to the title. Close returns focus to the opener.
- **Tokens** — `--panel`, `--shadow`, `--z-overlay`, backdrop tint.

### Drawer / Sheet ⬜

A panel that slides from an edge — filters, record detail, mobile nav.

- **Variants** — right/left/bottom; sizes; with backdrop or push-content.
- **A11y** — same focus discipline as the modal.

### Popover ⬜

A small floating panel anchored to a trigger — richer than a tooltip, can hold
controls.

- **A11y** — `aria-expanded` on the trigger; Esc + outside-click close.

### Tooltip ⬜

A tiny label on hover/focus for an icon or truncated text.

- **A11y** — `aria-describedby` links trigger to tip; must appear on **focus**,
  not hover only; never put essential info or interactive content in a tooltip.

### Context menu ⬜

Right-click actions on a row or item (reuses the dropdown `menu`).

---

## Layout primitives

Low-level building blocks; no chrome of their own.

### Container ✅

Centres content at `max-width: 1080px` with gutters. (`container`.)

### Grid ⬜

Token-gap responsive columns: `grid-2`/`grid-3`/`grid-4`, collapsing at `md`.
Already used ad-hoc (`features`, `stats`, `grid-3`); to be generalised.

### Stack ✅

Vertical rhythm — equal gap between children (`stack`).

### Cluster ⬜

Horizontal wrap with consistent gap — button rows, tag lists, toolbars.

### Split ✅

Two-column, content + media/code, collapsing at `md` (`split`).

### Divider ⬜

A labelled or plain horizontal rule between sections (`divider`).

### Page header ✅

Eyebrow + title + lede + optional action row (`page-head`). The standard top of
every docs and admin page.

### Section ✅

A top-bordered vertical band of content (`comp-cat` pattern), generalised to
`section`.

---

## Admin shell (PocketBase-style) ⬜

The composed surfaces the framework ships so a new project has a working admin UI
on day one. These assemble the primitives above.

### App shell ⬜

The frame: top app bar + collapsible collection sidebar + content area + toast
region. Responsive: sidebar becomes a drawer at `md`.

### Collection sidebar ⬜

Lists collections (from `collections.toml`) with active highlight, search, and a
"new collection" action.

### Record table ⬜

The data table wired to a collection: server-driven columns, sort, the
`?search=` box, row selection, bulk actions, pagination, and an empty state.

### Record editor ⬜

An auto-generated form from a collection's fields — each field type maps to a
control (text→input, bool→switch, relation→combobox, embed-text→textarea), with
inline validation, dirty-state guard, and Save/Cancel in `form-actions`.

### Filter bar ⬜

Compose a query against the records DSL: field + operator + value chips, AND/OR,
saved views.

### Auth / login form ⬜

Centered card: email + password (or magic-link), error alert, submit with loading
state. The front door of every admin install.

### Settings panel ⬜

Tabbed forms for instance settings — sectioned `form-section`s with switches and
inputs, one Save per section.

---

## What "done" means

A component graduates from ⬜ → 🟡 → ✅ when, in order: (1) its markup renders
from the `crates/css` utility/atomic output with no hand-written one-off CSS;
(2) every state listed above is reachable and visible; (3) the accessibility
requirements pass a keyboard-only and screen-reader check; (4) it appears on the
live `/components` page with a usage example. Track status changes here in the
same commit as the implementation, the way the crate map is tracked in
`AGENTS.md`.