Skip to content

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.

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

    IDStatusUpdated
    Waiting for pushed rows…
    <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.

    retry: 5000
    event: activity:item
    data: <li class="hc-item">Deploy #42 started</li>
    event: status:panel
    data: <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.

    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.

    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.

    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).