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 |