HTMX Patterns

OOB helpers, suspense slots, SSE status, nav progress, form a11y — server-driven UI patterns.

Navigation Progress

CSS-only progress bar fixed to the viewport top. Animates automatically when body.htmx-request is present. Place once in your base layout. app_shell has its own built-in bar — use nav_progress() for pages outside the shell.

Preview above (contained). In production it sits at position:fixed; top:0.

OOB Helpers

oob.html provides macros for composing htmx out-of-band swaps. Include them in any htmx response to update multiple page regions at once.

oob_fragment

Wrap any content with hx-swap-oob. The swap parameter defaults to "true" (outerHTML); use "innerHTML" to replace children only.

This content replaces #my-target via OOB swap.

oob_toast

Shorthand for appending a toast notification from any htmx response. Renders a toast() with oob=true targeting the toast container.

oob_toast("Item saved!", variant="success")
oob_toast("Something went wrong.", variant="error")

counter_badge

Numeric indicator for navigation items, tabs, and sidebars. Supports max_count (shows "99+" by default), variant (warning, danger), and oob=true for server-driven updates. Hidden automatically when count=0.

Default

5

Warning

12

Danger

3

Overflow (99+)

99+

Suspense

Skeleton-to-content swap pattern for deferred loading. The server renders the shell immediately with skeleton placeholders, then sends deferred content as OOB swaps targeting each slot's id. Pairs with Chirp's Suspense(defer_map={}).

suspense_slot (default skeleton)

Renders a generic skeleton placeholder. When the server sends an OOB swap targeting this id, the skeleton is replaced with real content.

suspense_slot (typed skeleton)

Use skeleton_variant and lines for shaped placeholders.

Card skeleton

Text skeleton (3 lines)

suspense_slot (custom placeholder)

Use call blocks to provide custom loading content.

Loading stats...

suspense_group

Marks a region aria-busy="true" until all child slots resolve. Assistive tech announces the region as loading; remove aria-busy when the last slot receives its OOB swap.

SSE Status

Connection status indicators and error recovery for SSE streams. Pair with streaming_bubble / streaming_block.

sse_status

Dot + label indicator showing connection state. Uses role="status" and aria-live="polite" for screen readers.

Connected
Disconnected
Connection lost

sse_retry

Button that re-fetches an SSE endpoint to reconnect after failure. Targets the closest [hx-ext] ancestor by default.

Use inside an error state to let users manually recover:

streaming_bubble(role="assistant", streaming=false)
  <p>Connection lost.</p>
  sse_retry("/api/stream/123")
end

Form Accessibility

Server-rendered form patterns for accessible validation and htmx integration.

form_error_summary

Alert-style box at the top of a form listing all field errors with anchor links. Renders nothing when there are no errors. Supports oob=true for partial-page updates.

Empty state (hidden, but present in DOM for OOB targeting):

Safe-by-default forms

The form() macro auto-adds hx-select="unset" and hx-disinherit="hx-select" when htmx is detected (any hx_post, hx_put, etc.). This prevents hx-select inherited from boosted layouts from stripping OOB swaps in form responses.

This form has hx-post, so it automatically gets hx-select="unset", hx-sync="this:drop", and disabled submit controls while the request is in flight. Explicit hx_select, hx_sync, and hx_disabled_elt override the defaults.

Per-field OOB

Each field has id="field-{name}" and an error container id="errors-{name}" with role="alert". Set oob=true on field_wrapper to swap individual fields without re-rendering the whole form.

The error container (#errors-username) is always in the DOM. Server returns just the field with oob=true to update it in-place.