Skip to content

Kit catalog

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

Pure slots: presentation only

These hold 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

These are 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 labelled runtime stand-in. Use the component: this table is the canonical reference. The :binder column is the kit's internal wiring, which you do not 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] (persists across 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 before any page loads. It catches the three mistakes 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 are silently dropped;
  • a typo'd attribute name (data-formatt="num"): an unknown data-* is 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). If you generate 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 from the native element, with no runtime. The accessible default for a fixed option set; <Combobox> is the searchable upgrade.

Class-contract variants (no component needed)

These are classes you write directly: 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. There is nothing to instantiate.

Slots & props: at a glance

This table gives the exact contract of each component, so you can place one without opening the source. + body means 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)