Skip to content

Recipes

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