SSE toast
sse-toast is the blessed pattern for server-initiated
notifications: the server pushes a named SSE event with a JSON
payload, and one hidden bridge element turns it into a DOM
CustomEvent — so the existing
toast contract and the
data-region invalidation
contract work exactly as they do when HX-Trigger initiates, just
without a request. It is stable under the
markup versioning policy.
It needs installSseDispatch() and installToast() (both in the
auto-init
./behaviors
bundle) plus the
SSE extension.
Live demo
Section titled “Live demo”The stream plays a ~15 s scripted sequence once, then closes itself
with a demo:done event (named in data-sse-close): a sticky
Build #42 started toast updates in place to finished (the
update-by-id pattern), an items:changed push makes the region
below refetch (watch the Rendered at time), and a last info toast
arrives before the close. Reload the page to replay.
Items
The markup
Section titled “The markup”<div data-hx-ext="sse" data-sse-connect="/events"> <span hidden data-hc-sse-dispatch data-sse-swap="hc:toast, items:changed"></span></div>The bridge never renders and never receives focus. For each SSE event
named in its data-sse-swap, installSseDispatch() cancels the swap
and dispatches a bubbling CustomEvent instead — the SSE event name
becomes the DOM event name, the JSON payload becomes detail.
The stream
Section titled “The stream”event: hc:toastdata: {"message":"Build #42 finished","variant":"success"}
event: items:changeddata: {}hc:toastpayloads are the toastdetailshape, unchanged:message(required),title,variant,duration,id,action. Server-pushed progress can use the update-by-idpattern — repeated events with the sameidupdate one toast in place (loading → success).- Domain events are usually
{}: any data-region listening withdata-hx-trigger="items:changed from:body"refetches itself. The server pushes the invalidation; the region pulls the re-render — pushes stay tiny, re-renders stay idempotent, and reconnect gaps self-heal on the next event.
Payload rules
Section titled “Payload rules”data: | Result |
|---|---|
| empty / missing | dispatched, detail = {} |
| a JSON object | dispatched, that object is detail |
| anything else (array, string, number, malformed JSON) | dropped — swap still cancelled, nothing dispatched |
The markup is the allowlist. Only event names the page lists in
data-sse-swap can ever be dispatched — the server cannot mint DOM
event names the page did not declare. Payloads are inert
CustomEvent.detail data: never markup, never evaluated.
Accessibility
Section titled “Accessibility”The toast behavior’s semantics apply unchanged — variant: "error"
renders role="alert" (assertive), everything else role="status"
(polite). Server-pushed toasts arrive without user action, so prefer
polite variants and reserve error for interruptions worth making.
State the user must not miss belongs in the page (an
sse-updates region), not
only in a transient toast.
The payload rules and the region-invalidation composition are pinned by
a real-EventSource browser test (test-browser/sse.spec.mjs).
Related
Section titled “Related”- SSE live updates recipe — server-pushed fragments; this recipe’s sibling.
- Toast recipe — the
hc:toastdetail shape and region knobs. - Data region recipe — the invalidation contract the domain events drive.
- htmx integration — Server-sent events — extension setup and lifecycle.