SSE live updates
sse-updates is the blessed pattern for server-pushed UI: a feed,
status panel, or datagrid that re-renders because the server said so,
not because the user acted. The
htmx SSE extension
owns the EventSource; the markup declares the stream and the event
names; the server sends named events whose data is a finished
fragment — the same markup-as-wire-contract stance as every other
recipe. It is stable under the
markup versioning policy.
No Hypermedia Components behavior is needed for fragment pushes — for server-pushed notifications and domain events, see the SSE toast recipe.
Live demo
Section titled “Live demo”The stream plays a ~25 s scripted sequence once, then ends itself
deliberately with stream:done: timestamped activity items prepend
into the feed, the status panel re-renders, one push also updates
the alert badge out-of-band in the same event, and two products:rows
pushes swap a full page of rows into the datagrid’s tbody (the
composition below). Reload the page to
replay.
Waiting for updates…
Alerts:0
| ID | Status | Updated |
|---|---|---|
| — | Waiting for pushed rows… | — |
The markup
Section titled “The markup”<div data-hx-ext="sse" data-sse-connect="/events" data-sse-close="stream:done"> <ul id="activity" data-sse-swap="activity:item" data-hx-swap="afterbegin"> <li class="hc-item">…server-rendered current items…</li> </ul>
<section id="status" data-sse-swap="status:panel"> …server-rendered current status… </section></div>One data-sse-connect scope per stream. Each element inside names the
events it receives with data-sse-swap; the event’s data is swapped in
honouring the element’s data-hx-swap (innerHTML by default,
afterbegin for feeds). The extension supports the data- prefixed
attribute forms, matching the kit’s data-hx-* convention.
The stream
Section titled “The stream”retry: 5000
event: activity:itemdata: <li class="hc-item">Deploy #42 started</li>
event: status:paneldata: <p>All systems normal</p><span id="alert-badge" hx-swap-oob="true">3</span>Name events by domain (activity:item, status:panel) — the names are
wire contract. A message’s data may also carry hx-swap-oob fragments
that update other targets by id in the same push (the second example
updates both the panel and a badge), exactly like the
datagrid-pager and
bulk-actions responses.
Datagrid composition
Section titled “Datagrid composition”Push a page of rows by putting data-sse-swap +
data-hx-swap="innerHTML" on the .hc-datagrid__body tbody. The
keep-the-tbody rule from the pager applies unchanged: the grid’s
observer survives the swap, re-applies roles and sticky offsets, and
re-derives selection — so a
selection actions bar
clears itself when pushed rows arrive, with no extra wiring.
Lifecycle
Section titled “Lifecycle”Reconnection is htmx-native: EventSource retries automatically
(honouring the server’s retry: hint) and the extension adds backoff
on repeated failures — so the server must expect reconnects and resend
current state, or tolerate the gap. End streams deliberately with
data-sse-close="<event>" (a dropped stream reconnects; a closed one
stays closed). htmx:sseOpen / htmx:sseError / htmx:sseClose fire
on the scope for debugging. The extension connects with
withCredentials: true — cookies ride along.
No-JS degradation
Section titled “No-JS degradation”Render the complete current state into the initial HTML; the stream
only freshens it. Without JS the page is simply as fresh as the last
full render — no loading shells, nothing breaks. This is the recipe’s
accessibility stance too: pushed fragments replace content without
moving focus, and regions that should announce updates opt in with
aria-live="polite" on the landing element.
The claims here are pinned by a real-EventSource browser test
(test-browser/sse.spec.mjs).
Related
Section titled “Related”- SSE toast recipe — server-pushed notifications and domain events via the dispatch bridge.
- htmx integration — Server-sent events — loading the extension, lifecycle, credentials.
- Datagrid pager recipe — the tbody swap rules this composes with.
- Data region recipe — pull-based refresh, the complementary pattern.