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.
Also known as: auto-refresh, polling region.
Live demo
Section titled “Live demo”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
Polling on a schedule
Section titled “Polling on a schedule”<section id="orders-summary" class="hc-data-region" data-hx-get="/orders/summary" data-hx-trigger="load, every 10s" data-hx-swap="outerHTML" data-hx-indicator="closest .hc-data-region" aria-busy="false"> <header class="hc-data-region__header"> <h2>Orders</h2> <span class="hc-spinner htmx-indicator" aria-hidden="true"></span> </header> <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. The server’s re-rendered fragment carriesevery 10sonly: htmx re-arms the interval on each swap (that restart is the poll), and echoingloadback would refetch forever.data-hx-swap="outerHTML"— the region replaces itself with the complete<section>the server returns (same id, class, and attributes), so every swap is idempotent and the server stays in control of the region’s triggers.data-hx-indicator="closest .hc-data-region"— a slow poll lights up the header’s.hc-spinner.htmx-indicatorinstead of flickering the content.
Always design the polled endpoint to be cheap — a fast COUNT query or a denormalized read model — and idempotent.
Inner-content fragments (innerHTML)
Section titled “Inner-content fragments (innerHTML)”When you are retrofitting an existing fragment endpoint that returns only the inner content (no wrapper element), keep the triggers on a bare container and swap its contents instead:
<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>- The container itself (with its triggers) stays put; only its contents are replaced, so there is nothing to re-arm and the trigger keeps ticking as written.
- The trade-off: the server can no longer adjust the region — change
the poll interval, stop polling, listen for a new event — because
the fragment carries no attributes. Prefer the self-replacing
hc-data-regionform above when you control the server, and reserve this form for retrofits.
Whole-region self-replacement (hx-select)
Section titled “Whole-region self-replacement (hx-select)”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 a fragment
endpoint (the self-replacing
hc-data-regionform, or theinnerHTMLform above) when you control the server, and reservehx-selectfor retrofits. - With
outerHTMLthe region’severy 15stimer restarts on each swap; with noloadin the trigger the first paint is the server-rendered HTML, so there is no flash.
Event-driven refresh
Section titled “Event-driven refresh”Pair polling with an htmx custom event so server actions can ask the region to refresh immediately:
<section id="orders-summary" class="hc-data-region" data-hx-get="/orders/summary" data-hx-trigger="load, every 30s, orders:refresh from:body" data-hx-swap="outerHTML" data-hx-indicator="closest .hc-data-region" aria-busy="false"> …</section>The server fires the event via HX-Trigger on any write endpoint:
HTTP/1.1 200 OKHX-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.
Reveal-driven (lazy initial load)
Section titled “Reveal-driven (lazy initial load)”For data regions below the fold, defer the initial fetch until the region scrolls into view:
<section id="invoices-summary" class="hc-data-region" data-hx-get="/invoices/summary" data-hx-trigger="intersect once, every 30s" data-hx-swap="outerHTML" data-hx-indicator="closest .hc-data-region" aria-busy="false"> <p class="hc-field__message">Loading when visible…</p></section>intersect oncefires when the element enters the viewport, once.- The subsequent
every 30sonly starts ticking after the first fetch.
Server response contract
Section titled “Server response contract”The server returns the complete <section> — same id, same
class, same attributes — so the swap is idempotent, with one
deliberate difference: the re-rendered fragment drops load from
the trigger (htmx fires load on every freshly swapped element, so
echoing it back would refetch forever):
<!-- GET /orders/summary --><section id="orders-summary" class="hc-data-region" data-hx-get="/orders/summary" data-hx-trigger="every 10s" data-hx-swap="outerHTML" data-hx-indicator="closest .hc-data-region" aria-busy="false"> <header class="hc-data-region__header"> <h2>Orders</h2> <span class="hc-spinner htmx-indicator" aria-hidden="true"></span> </header> <dl class="hc-stack"> <div> <dt>Open orders</dt> <dd>14</dd> </div> <div> <dt>Awaiting payment</dt> <dd>3</dd> </div> </dl></section>Because the fragment carries the region’s attributes, the server
stays in control: it can change the poll interval, stop polling once
the data is final, and render an explicit empty state when there are
no rows. For the innerHTML retrofit
form, return only the inner
content (the <dl> above) instead.
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.
| Request | Response |
|---|---|
GET /orders/summary (poll, event, or reveal) | 200 + the complete <section> (innerHTML retrofit: the inner content only) |
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 region | 2xx + HX-Trigger: {"orders:refresh":true} |
GET /orders/summary (failure) | non-2xx — not swapped; the region keeps its previous rendering |
On failure
Section titled “On failure”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).
Indicators
Section titled “Indicators”Wrap a spinner in htmx-indicator if the polling endpoint is slow
enough that flickering content matters. The hc-data-region form
above already wires this up with
data-hx-indicator="closest .hc-data-region". For the innerHTML
form, put the request attributes on an element that contains the
spinner, so the indicator selector can reach it (closest walks
ancestors only; find searches descendants):
<section id="status" data-hx-get="/status" data-hx-trigger="load, every 5s" data-hx-target="#status-body" data-hx-swap="innerHTML" data-hx-indicator="find header .hc-spinner"> <header> <h2>Sync status</h2> <span class="hc-spinner htmx-indicator" aria-hidden="true"></span> </header> <div id="status-body">…</div></section>Accessibility
Section titled “Accessibility”- Mark the region with
aria-live="polite"when the user benefits from being told about changes (e.g. counters), oraria-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.
Progressive enhancement
Section titled “Progressive enhancement”- 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 5sfor 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.
Related
Section titled “Related”- Live search recipe — for user-driven updates rather than time-driven.
- Toast recipe — pair with
HX-Triggerfor cross-region notifications.