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.

RecipeYou need to…Needs
Mutating formpost a form with inline 4xx errors, a success redirect, and a double-submit guardinstallFieldErrors()
Field errorsdistribute a server-sent validation-error fragment to the fields it namesinstallFieldErrors()
Conditional fieldshide the fields the chosen mode does not read — declaratively, without a round-tripinstallShowWhen()
Multi-step formbuild a wizard — the server owns the step and the draft; whole-step swaps
Inline editswap a value between display and edit modes in place (click-to-edit)
File uploadupload multipart files with a live progress bar and an out-of-band form resetinstallUploadProgress()
Postal addressfill the address from a postal code — masked input, one lookup GET, out-of-band autofillinstallMask()
Reference lookupreference a master record — direct code entry validated on change, a search-dialog picker, and a hidden id an unresolved code always clearsinstallRemoteDialog() + installCloseDialog()
Cascading selectchain dependent selects for hierarchical selection; each change loads the next level
Transferassign / 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.

RecipeYou need to…Needs
Unsaved changeswarn before edits are lost — data-dirty state, tab-close prompt, boosted-nav confirm, clean-on-saveinstallDirtyGuard()
Autosavedraft the form as the user types — debounced posts, restore banner, zero new JS
Edit conflictoptimistic locking — a hidden version rides every save; stale saves get a 409 conflict dialog
Idempotency keymake duplicate submits safe server-side — one key per rendered form; a replayed key gets the original response, never an error
Session expiryturn a 401 into a login dialog and replay the interrupted requestinstallSessionExpiry()
Network retrysurface the request that got no answer at all (offline / timeout) with a working RetryinstallNetworkRetry()

One element, one request — buttons, guards, and small client-side utilities.

RecipeYou need to…Needs
Request actionfire an htmx request from a button with a busy state
Confirm actionguard a destructive request behind a confirmation dialoginstallConfirm()
Undo deletedelete immediately (optimistic delete) — the toast’s Undo restores the row within a grace periodinstallToast()
Copy to clipboardcopy a read-only value (URL, token, snippet) to the clipboardinstallCopy()
Sortable listreorder a server-owned list with pointer or keyboard and persist the order in one requestinstallSortable()

The contracts business records move through — line items, lifecycles, bulk imports, and long-running work.

RecipeYou need to…Needs
Line itemsedit order/quote detail rows — add, remove, and recalculate as whole-form round trips; the server owns all arithmetic
Workflow actionsdrive a record’s lifecycle — the server renders only the legal transitions, comment-required transitions 422, stale ones 409 from current truth
CSV importbulk rows in without surprises — upload, validation report, tokened confirm
Async jobrun 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.

RecipeYou need to…Needs
Datagrid paginationpage a datagrid server-side with out-of-band row swapsinstallDatagrid()
Datagrid sortsay what the sort set IS and let anyone edit it — an ordered, reorderable list of keys, including keys on columns that are not showninstallSortList()
Datagrid filterper-column filter popovers — namespaced f-col params GET the grid and the trigger comes back data-filtered
Datagrid columnslet each user choose the grid’s columns — a chooser form GETs the grid with repeated cols= params
Datagrid prefsremember per-user column widths and order — mirrored inputs autosave resizes; the sortable chooser’s cols= sequence is the order
Row detailopen 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 selectioninstallRowLink()
Datagrid treehierarchy inside the grid — aria-level rows lazy-load their children right after the parent
Datagrid infiniteendless lists without page numbers — a revealed sentinel row swaps in the next cursor batch
Datagrid snapshot pagerfreeze a work queue’s membership at search time — form-carried row keys, processed rows stay visible, vanished rows tombstoneinstallDatagrid()
Datagrid edit errorsclose the grid’s optimistic-commit loop — 422 re-renders the record with the marked cell + error row
Datagrid edit conflictoptimistic locking per row — a stale version 409s into a conflict row with overwrite/discard
Datagrid bulk actionsselect rows with checkboxes and POST one action against all of theminstallDatagridActions()
Datagrid bulk errorsbulk failures at scale — best-effort vs atomic, reasons grouped, rows linked from the report
RecipeYou need to…Needs
Live searchsearch as you type (typeahead) — debounced input swaps the results
Result capbound what one search may return — cap+1 detection, “cap+” counts, a persistent truncation banner or a hard reject
Filter popoverhost a filter form in a popover that closes on successinstallClosePopover()
Saved viewsname the current filters and get them back as plain links — server-stored querystrings
RecipeYou need to…Needs
Lazy paneldefer loading a panel until it is revealed
Lazy treeload a tree branch’s children on first expandinstallTree()
Data regionrefresh a region on HX-Trigger invalidations (self-refreshing region)
RecipeYou need to…Needs
SSE updatespush fragment updates from the server over Server-Sent Events — named events and out-of-band pushes
SSE toastturn SSE events with JSON payloads into allow-listed DOM events; toasts work unchangedinstallSseDispatch()
Chat messagesappend a chat exchange — user message + aria-busy assistant placeholder — from one composer POST, with a 422 path that never pollutes the transcriptinstallChatScroll()
Streaming responsefill the assistant placeholder over SSE — chunks append while aria-busy defers the announcement; done swaps in the final message and closes the stream
RecipeYou need to…Needs
Remote dialogload a dialog’s (modal’s) contents from the server on openinstallRemoteDialog() + installCloseDialog()
Toastfire toast notifications (snackbars) from the client or an HX-Trigger headerinstallToast()
Unread badgekeep the notification count in app chrome fresh, honest at zero, and corrected by your own actions
RecipeYou need to…Needs
Chartupgrade a server-sent data table into an Observable Plot SVG chartinstallChart() + Plot