Skip to content

Data region

A data region is a piece of UI that owns its own refresh lifecycle. It loads once on page render and then keeps itself up to date by polling, by listening for events, or by combining both. The component is just an htmx-driven container — no behavior helper needed.

Press Simulate a change elsewhere — the endpoint answers 204 with HX-Trigger: {"items:changed":{}, "hc:toast":…}, and the region hears the event and refetches itself (watch the Rendered at time change). The endpoint is a namespaced demo implementation of the server response contract under api/recipes/data-region/.

The second region (Polling region) demos polling on a schedule: it refetches itself every 10 seconds — its re-rendered fragment carries every 10s only, and htmx re-arms the interval on each swap. Watch its Rendered at line tick over.

Items

Polling region

<section id="orders-summary"
data-hx-get="/orders/summary"
data-hx-trigger="load, every 10s"
data-hx-swap="innerHTML">
<p class="hc-field__message">Loading…</p>
</section>

Pieces:

  • data-hx-trigger="load, every 10s" — render once on page load, then again every 10 seconds.
  • data-hx-swap="innerHTML" — replace the container’s contents only; the container itself (with its triggers) stays put.

Always design the polled endpoint to be cheap — a fast COUNT query or a denormalized read model — and idempotent.

When the endpoint can only return the full page (a server-rendered view with no fragment route), let the region cut its replacement out of that page and swap itself:

<div id="page-content"
data-hx-get="/ops/console/outbox"
data-hx-trigger="every 15s"
data-hx-select="#page-content"
data-hx-target="this"
data-hx-swap="outerHTML">
</div>
  • data-hx-select="#page-content" extracts the matching element from the response; data-hx-swap="outerHTML" replaces the region with it, triggers and all.
  • This costs a full page render per tick — prefer the fragment endpoint + innerHTML form above when you control the server, and reserve hx-select for retrofits.
  • With outerHTML the region’s every 15s timer restarts on each swap; with no load in the trigger the first paint is the server-rendered HTML, so there is no flash.

Pair polling with an htmx custom event so server actions can ask the region to refresh immediately:

<section id="orders-summary"
data-hx-get="/orders/summary"
data-hx-trigger="load, every 30s, orders:refresh from:body"
data-hx-swap="innerHTML">
</section>

The server fires the event via HX-Trigger on any write endpoint:

HTTP/1.1 200 OK
HX-Trigger: {"hc:toast":{"message":"Created"},"orders:refresh":true}

from:body listens for the event on document.body, which is where htmx dispatches HX-Trigger events.

For data regions below the fold, defer the initial fetch until the region scrolls into view:

<section id="invoices-summary"
data-hx-get="/invoices/summary"
data-hx-trigger="intersect once, every 30s"
data-hx-swap="innerHTML">
<p class="hc-field__message">Loading when visible…</p>
</section>
  • intersect once fires when the element enters the viewport, once.
  • The subsequent every 30s only starts ticking after the first fetch.

The server returns only the inner content of the region:

<!-- GET /orders/summary -->
<dl class="hc-stack">
<div>
<dt>Open orders</dt>
<dd>14</dd>
</div>
<div>
<dt>Awaiting payment</dt>
<dd>3</dd>
</div>
</dl>

Return the same content for both htmx (HX-Request: true) and full-page loads. Caching can be tuned with standard Cache-Control headers; htmx respects them.

RequestResponse
GET /orders/summary (poll, event, or reveal)200 + the region’s inner content fragment
GET /orders/summary (no HX-Request — full page load)200 + the same content, wrapped in the full page
Any write endpoint that should refresh the region2xx + HX-Trigger: {"orders:refresh":true}
GET /orders/summary (failure)non-2xx — not swapped; the region keeps its previous rendering

htmx ≥ 2 does not swap non-2xx responses, so a failed poll leaves the previous rendering visible — the region degrades to slightly stale, never to broken. To tell the user, add an HX-Trigger: {"hc:toast":{…}} header to the error response (see the toast recipe).

Wrap a spinner in htmx-indicator if the polling endpoint is slow enough that flickering content matters:

<section id="status">
<header>
<h2>Sync status</h2>
<span class="hc-spinner htmx-indicator" aria-hidden="true"></span>
</header>
<div
data-hx-get="/status"
data-hx-trigger="load, every 5s"
data-hx-target="#status-body"
data-hx-swap="innerHTML"
data-hx-indicator="closest header .hc-spinner">
<div id="status-body"></div>
</div>
</section>
  • Mark the region with aria-live="polite" when the user benefits from being told about changes (e.g. counters), or aria-live="off" if updates are noisy and decorative.
  • Do not move focus on each refresh. Polling that hijacks focus is hostile to keyboard users and screen readers alike.
  • Show an explicit empty state when the data set becomes empty so assistive tech does not encounter a silently empty section.
  • Without JavaScript, the region renders its initial server-side contents and never refreshes. Render reasonable defaults inside the section instead of leaving it blank.
  • Without htmx, the section behaves as plain HTML.
  • Background tabs throttle timers. Browsers slow down polling in inactive tabs; do not rely on every 5s for real-time accuracy.
  • Sync points. Pair polling with SSE or WebSockets when sub-second freshness matters. The data region recipe stays the same — only the trigger changes.
  • Cost. Per-second polling at scale is expensive. Prefer event-driven refresh (from:body) once you have a backplane.