Skip to content

Kit catalog

24 components, grouped by how they hold state. The gallery (/ui-kit in the demo umbrella) shows every one live, and the browser e2e drives them all.

Pure slots — presentation only

No state; the parent projects content.

Component Notes
<Card> title / actions slots + default body
<Field> label / help / error around a control — also the substrate declui generates onto
<PageHeader> title + actions slot
<Stat> a KPI tile
<EmptyState> icon + message + action
<Avatar> initials or image; sizes via forwarded class
<Breadcrumb> links with CSS separators
<Tooltip> CSS hover tooltip

Attribute forwarding applies to every component: class on the host merges into the child's root; style/id/title/role/tabindex/aria-*/data-* are forwarded. A mistyped prop (anything else) fails closed.

Controlled — the parent owns the flag

Externally-triggered overlays: the opener lives elsewhere on the page, so the parent holds the Local and the wiring (:open prop + @close emit).

Component Contract
<Dialog> :open + @close; footer content via slots
<Drawer> side panel; same contract
<Toast> transient notice; pairs well with :autohide
<button @click="confirm_open = True" class="ui-btn ui-btn--primary">Delete…</button>
<Dialog :open="confirm_open" @close="close_confirm">
  <template slot="title">Delete 3 issues?</template>
  <button class="ui-btn ui-btn--danger" @click="do_delete()">Delete</button>
</Dialog>

Uncontrolled — the component owns its state

Self-triggered widgets: drop them in with zero parent wiring; each placement is monomorphized into independent state.

Component Notes
<Collapsible> an accordion row; N placements, N independent open flags
<Popover> trigger + panel, outside-click close
<Switch> on/off toggle
<TagInput> add/remove chips over a Local[list] (the client :each reconciler)
<Sortable> pointer drag-reorder via :sortable

Widget primitives — the optional runtime family

Complex widgets whose presentation state (open option, sort column, page) nothing else reads. They ship as a runtime in widgets.js; your data and selection stay in Locals the app reads. Each is a deliberate, labelled runtime stand-in. Use the component — this table is the canonical reference; the :binder column is the kit's internal wiring, not something you author directly.

Component (the API) Binder (internal) State the app owns
<Tabs> :tabs — (bar generated from data-tab panels)
<DataTable> :table="rows" a Local[list]; header-click sorting
<DataGrid> :datagrid="rows" + :datagridsel="sel" rows Local[list] + selection Local[set] (survives sort/filter/page; tri-state select-all; aria-sort headers)
<Combobox> :combobox Local[str] — searchable single-select, full keyboard + ARIA listbox
<ComboboxMulti> :combobox (multi) Local[set] — chips, Backspace removes last
<Menu> :menu — (roving focus, Escape-to-trigger; each item keeps its compiled @click)
<CommandPalette> :palette — (Ctrl/⌘-K overlay, fuzzy filter, activates the item's compiled @click)

DataGrid columns

<DataGrid> (and <DataTable>) read their columns from the <th data-field> cells in the head slot; each <th> carries optional data-* options. Hand-author them, or let declui's screens=("table",) generate them from a typed model.

Attribute Effect
data-field="key" required — the row-object key this column reads
data-sort="off" make the column unsortable (sortable by default; shift-click a header for a secondary key)
data-sort-type="numeric" \| "date" sort on the raw value numerically / chronologically (default: natural text)
data-format="money" \| "date" \| "datetime" \| "num" \| "percent" \| "bool" a display formatter (closed registry; an unknown key throws) — the raw value stays for sort/filter
data-filter="text" render a per-column filter input in a filter row (ANDs with the global box)
data-align="right" \| "center" cell alignment
data-hidden start the column hidden (the toolbar's Columns picker can restore it)
data-pin freeze the column to the left under horizontal scroll (also toggleable from the Columns menu)

<DataGrid>'s toolbar adds a global filter, a Columns menu (visibility + pin toggles), and a page-size selector + a "Showing X-Y of Z" line. On the headers: drag the right edge to resize, drag the left grip to reorder, click to sort. Data and selection stay in the app's Locals; cells are textContent-only (never HTML). <DataTable> is the minimal sortable subset (no toolbar).

The column contract fails closed — at the lint gate

situ check validates every <th> in your tree, so a column mistake fails the gate rather than the page. It catches the three the runtime cannot see, because a <th> the engine never reads is a <th> it cannot reject:

  • a knob on a <th> with no data-field — the engine selects thead th[data-field], so the column and every knob on it is silently dropped;
  • a typo'd attribute name (data-formatt="num") — an unknown data-* is simply never read;
  • a value on a presence-only knob: data-hidden and data-pin are tested with hasAttribute, so data-pin="false" pins the column. Write the bare attribute or omit it.

data-sort is likewise closed to "off" — the engine tests !== "off", so data-sort="of" would silently mean sortable. Unknown values on any knob are rejected by both the lint check (when literal) and the runtime (always). Generating columns from a typed model? declui's table screen emits this contract from Field(...), checked against the same closed sets by the type checker.

Native-wrapped

Component Notes
<Select> a real <select class="ui-select"> with :bind forwarding — keyboard and screen-reader behavior for free, zero runtime. The accessible default for a fixed option set; <Combobox> is the searchable upgrade.

Class-contract variants (no component needed)

Buttons (ui-btn, --primary/--danger/--ghost, sizes, loading), inputs (ui-input, invalid state), ui-badge--*, ui-alert--*, tables + pagination, ui-progress, ui-skeleton, ui-kbd. Write the class; there is nothing to instantiate.

Slots & props — at a glance

The exact contract of each component, to place it without opening the source. + body = the default (unnamed) slot; binders and the state-mode rationale are in the family sections above. Every component also forwards class + the passthrough attributes.

Component Props / emits Named slots (+ body)
<Avatar> + body (initials)
<Breadcrumb> + body (links)
<Card> title, actions, + body
<PageHeader> title, description, actions
<Stat> label, delta, + body (value)
<EmptyState> media, title, description, action
<Field> label, help, + body (the control)
<Tooltip> tip, + body (trigger)
<Dialog> open: Prop[bool], close: Emit[None] title, footer, + body
<Drawer> open: Prop[bool], close: Emit[None] title, + body
<Toast> open: Prop[bool], close: Emit[None] + body
<Collapsible> (owns its open flag) title, + body
<Popover> (owns its open flag) trigger, + body (panel)
<Menu> trigger, + body (items)
<Switch> (owns on: Local[bool]) + body (label)
<TagInput> (owns tags: Local[list])
<Sortable> (owns items: Local[list]; v1)
<Tabs> (runtime-only; panels via data-tab) + body (panels)
<DataTable> rows: Prop[list] head
<DataGrid> rows: Prop[list], selection: Prop[set] head
<Combobox> value: Prop[str] + body (options)
<ComboboxMulti> value: Prop[set] + body (options)
<CommandPalette> + body (items)
<Select> value: Prop[str] + body (<option>s)