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--modifierstyle, 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
<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
<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
menupanel →menu-items,
menu-separator, optional menu-label.
- States —
open, item:hover/:focus,is-disabled,is-destructive. - Markup
<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
<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>
- 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.
- 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
<input type="email" placeholder="ada@example.com" />
- A11y — always labelled (visible
<label>oraria-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;
rowssets 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 →
listboxpopup →options, 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
<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
<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 group) and form-actions ⬜ (footer button row, right-aligned).
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
<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
<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
<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
<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 —
altis the person's name; decorative grouping isaria-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/groupwitharia-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
<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
--panelviacolor-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) stackstoastitems. - 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 arearia-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
<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 —
sidebarwithsidebar-group+sidebar-title✅; collapsible
groups ⬜; active-item highlight ⬜ (drive off the active slug).
- A11y —
<nav aria-label>; current itemaria-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
<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 pagearia-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 →
dialogpanel → header (title + close) → body →
footer actions.
- States — opening, open, closing;
is-busywhile submitting. - Markup
<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-expandedon the trigger; Esc + outside-click close.
Tooltip ⬜
A tiny label on hover/focus for an icon or truncated text.
- A11y —
aria-describedbylinks 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-sections 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.