Each recipe documents the short form, the expanded HTML, the optional
macro, and the server response contract. Recipes with an entry in the
Needs column require an installX() from
@hypermedia-components/core;
the rest (”—”) are pure htmx attributes.
Get any recipe’s scaffold with npx @hypermedia-components/cli add <name>;
machine-check your copy against its contract with
npx @hypermedia-components/cli validate.
Every recipe page also has a Live demo. Almost all of them make
real htmx requests against a demo API served by this site’s Worker,
implementing that recipe’s server response contract under
api/recipes/<name>/ (the exceptions: copy,
conditional fields,
and unsaved changes
are client-only, and streaming response
shares the chat-messages endpoint).
Build and submit forms — validation, wizards, and the inputs that need
a server round trip.
| Recipe | You need to… | Needs |
|---|
| Mutating form | post a form with inline 4xx errors, a success redirect, and a double-submit guard | installFieldErrors() |
| Field errors | distribute a server-sent validation-error fragment to the fields it names | installFieldErrors() |
| Conditional fields | hide the fields the chosen mode does not read — declaratively, without a round-trip | installShowWhen() |
| Multi-step form | build a wizard — the server owns the step and the draft; whole-step swaps | — |
| Inline edit | swap a value between display and edit modes in place (click-to-edit) | — |
| File upload | upload multipart files with a live progress bar and an out-of-band form reset | installUploadProgress() |
| Postal address | fill the address from a postal code — masked input, one lookup GET, out-of-band autofill | installMask() |
| Reference lookup | reference a master record — direct code entry validated on change, a search-dialog picker, and a hidden id an unresolved code always clears | installRemoteDialog() + installCloseDialog() |
| Cascading select | chain dependent selects for hierarchical selection; each change loads the next level | — |
| Transfer | assign / unassign across a dual listbox as a server round trip — whole-form swaps, zero custom JS | — |
Protect work in progress — drafts, dirty state, expired sessions,
dropped connections, and conflicting or duplicate saves.
| Recipe | You need to… | Needs |
|---|
| Unsaved changes | warn before edits are lost — data-dirty state, tab-close prompt, boosted-nav confirm, clean-on-save | installDirtyGuard() |
| Autosave | draft the form as the user types — debounced posts, restore banner, zero new JS | — |
| Edit conflict | optimistic locking — a hidden version rides every save; stale saves get a 409 conflict dialog | — |
| Idempotency key | make duplicate submits safe server-side — one key per rendered form; a replayed key gets the original response, never an error | — |
| Session expiry | turn a 401 into a login dialog and replay the interrupted request | installSessionExpiry() |
| Network retry | surface the request that got no answer at all (offline / timeout) with a working Retry | installNetworkRetry() |
One element, one request — buttons, guards, and small client-side
utilities.
| Recipe | You need to… | Needs |
|---|
| Request action | fire an htmx request from a button with a busy state | — |
| Confirm action | guard a destructive request behind a confirmation dialog | installConfirm() |
| Undo delete | delete immediately (optimistic delete) — the toast’s Undo restores the row within a grace period | installToast() |
| Copy to clipboard | copy a read-only value (URL, token, snippet) to the clipboard | installCopy() |
| Sortable list | reorder a server-owned list with pointer or keyboard and persist the order in one request | installSortable() |
The contracts business records move through — line items, lifecycles,
bulk imports, and long-running work.
| Recipe | You need to… | Needs |
|---|
| Line items | edit order/quote detail rows — add, remove, and recalculate as whole-form round trips; the server owns all arithmetic | — |
| Workflow actions | drive a record’s lifecycle — the server renders only the legal transitions, comment-required transitions 422, stale ones 409 from current truth | — |
| CSV import | bulk rows in without surprises — upload, validation report, tokened confirm | — |
| Async job | run work that outlives a request — 202 + a self-polling job card with enumerated terminal states | — |
Everything the hc-datagrid
component is operated with — paging, the sort/filter/column tooling,
inline editing’s error wires, and the patterns that keep large grids
honest. The Data grid page
template composes the layout they all sit in; new to the subsystem?
Start with the Data grid guide,
which maps all of it in build order.
| Recipe | You need to… | Needs |
|---|
| Datagrid pagination | page a datagrid server-side with out-of-band row swaps | installDatagrid() |
| Datagrid sort | say what the sort set IS and let anyone edit it — an ordered, reorderable list of keys, including keys on columns that are not shown | installSortList() |
| Datagrid filter | per-column filter popovers — namespaced f-col params GET the grid and the trigger comes back data-filtered | — |
| Datagrid columns | let each user choose the grid’s columns — a chooser form GETs the grid with repeated cols= params | — |
| Datagrid prefs | remember per-user column widths and order — mirrored inputs autosave resizes; the sortable chooser’s cols= sequence is the order | — |
| Row detail | open the record a row is about and come back to the same list — the identity-cell link, #row-<id> on return, and walking the result set or the selection | installRowLink() |
| Datagrid tree | hierarchy inside the grid — aria-level rows lazy-load their children right after the parent | — |
| Datagrid infinite | endless lists without page numbers — a revealed sentinel row swaps in the next cursor batch | — |
| Datagrid snapshot pager | freeze a work queue’s membership at search time — form-carried row keys, processed rows stay visible, vanished rows tombstone | installDatagrid() |
| Datagrid edit errors | close the grid’s optimistic-commit loop — 422 re-renders the record with the marked cell + error row | — |
| Datagrid edit conflict | optimistic locking per row — a stale version 409s into a conflict row with overwrite/discard | — |
| Datagrid bulk actions | select rows with checkboxes and POST one action against all of them | installDatagridActions() |
| Datagrid bulk errors | bulk failures at scale — best-effort vs atomic, reasons grouped, rows linked from the report | — |
| Recipe | You need to… | Needs |
|---|
| Live search | search as you type (typeahead) — debounced input swaps the results | — |
| Result cap | bound what one search may return — cap+1 detection, “cap+” counts, a persistent truncation banner or a hard reject | — |
| Filter popover | host a filter form in a popover that closes on success | installClosePopover() |
| Saved views | name the current filters and get them back as plain links — server-stored querystrings | — |
| Recipe | You need to… | Needs |
|---|
| Lazy panel | defer loading a panel until it is revealed | — |
| Lazy tree | load a tree branch’s children on first expand | installTree() |
| Data region | refresh a region on HX-Trigger invalidations (self-refreshing region) | — |
| Recipe | You need to… | Needs |
|---|
| SSE updates | push fragment updates from the server over Server-Sent Events — named events and out-of-band pushes | — |
| SSE toast | turn SSE events with JSON payloads into allow-listed DOM events; toasts work unchanged | installSseDispatch() |
| Chat messages | append a chat exchange — user message + aria-busy assistant placeholder — from one composer POST, with a 422 path that never pollutes the transcript | installChatScroll() |
| Streaming response | fill the assistant placeholder over SSE — chunks append while aria-busy defers the announcement; done swaps in the final message and closes the stream | — |
| Recipe | You need to… | Needs |
|---|
| Remote dialog | load a dialog’s (modal’s) contents from the server on open | installRemoteDialog() + installCloseDialog() |
| Toast | fire toast notifications (snackbars) from the client or an HX-Trigger header | installToast() |
| Unread badge | keep the notification count in app chrome fresh, honest at zero, and corrected by your own actions | — |
| Recipe | You need to… | Needs |
|---|
| Chart | upgrade a server-sent data table into an Observable Plot SVG chart | installChart() + Plot |