htmx Server-Sent Events (SSE) Extension
The SSE extension adds support for Server-Sent Events streaming to htmx. It works by intercepting any htmx response with Content-Type: text/event-stream and streaming SSE messages into the DOM in real-time.
SSE is a lightweight alternative to WebSockets that works over existing HTTP connections, making it easy to use through proxy servers and firewalls. SSE is uni-directional: the server pushes data to the client. If you need bi-directional communication, consider WebSockets instead.
Installing
Include the extension script after htmx and approve it:
<head>
<script src="/path/to/htmx.js"></script>
<script src="/path/to/ext/hx-sse.js"></script>
</head>
Approve the extension via meta tag:
<meta name="htmx-config" content='{"extensions": "sse"}'>
How It Works
The SSE extension hooks into htmx’s request pipeline. When any htmx request receives a response with Content-Type: text/event-stream, the extension takes over and streams SSE messages into the DOM instead of performing a normal swap.
This means any hx-get, hx-post, etc. that returns an SSE stream will just work, no special attributes needed beyond loading the extension.
hx-sse:connect
For persistent SSE connections (auto-connect on load, reconnect on failure), use hx-sse:connect:
<!-- Auto-connects on load, streams messages into the div -->
<div hx-sse:connect="/stream">
Waiting for messages...
</div>
hx-sse:connect is convenience sugar for a well-preconfigured hx-get. It defaults to:
- Trigger:
load(connects immediately) - Reconnect: enabled with exponential backoff
- Pause on background: closes the stream when the tab is backgrounded, reconnects when visible
Using with Standard Attributes
hx-sse:connect works with all standard htmx attributes:
<!-- Swap into a different target -->
<button hx-sse:connect="/notifications" hx-target="#alerts">
Start Notifications
</button>
<div id="alerts"></div>
<!-- Append messages instead of replacing -->
<div hx-sse:connect="/log" hx-swap="beforeend">
<h3>Log:</h3>
</div>
Trigger Modifiers
All standard hx-trigger modifiers are supported:
<!-- Connect after a delay -->
<div hx-sse:connect="/stream" hx-trigger="load delay:2s">
<!-- Connect on click -->
<button hx-sse:connect="/stream" hx-trigger="click">Start</button>
<!-- Connect on click, only once -->
<button hx-sse:connect="/stream" hx-trigger="click once">Start</button>
Using Standard htmx Attributes
Since the extension intercepts based on Content-Type, any htmx request that returns text/event-stream will be streamed automatically:
<!-- hx-get, hx-post, etc. all work -->
<div hx-get="/stream" hx-trigger="load">
Waiting...
</div>
<button hx-post="/generate" hx-target="#output">
Generate
</button>
The difference is that hx-sse:connect enables reconnection and pauseOnBackground by default, while standard attributes do not.
hx-sse:close
Use hx-sse:close to gracefully close an SSE connection when a specific named event is received from the server:
<div hx-sse:connect="/stream" hx-sse:close="done">
Streaming until server sends "done"...
</div>
When the server sends event: done, the connection is closed and an htmx:sse:close event is fired with detail.reason === "message".
Named Events
SSE messages with an event: field are dispatched as DOM events on the source element rather than being swapped:
event: notification
data: {"title": "New message", "body": "Hello!"}
<div hx-sse:connect="/events"
hx-on:notification="alert(event.detail.data)">
</div>
Messages without an event: field are swapped into the DOM as HTML content.
Configuration
Configure SSE behavior globally via htmx.config.sse or per-element via hx-config:
<!-- Global config -->
<meta name="htmx-config" content='{
"sse": {
"reconnect": true,
"reconnectDelay": 500,
"reconnectMaxDelay": 60000,
"reconnectMaxAttempts": 50,
"reconnectJitter": 0.3,
"pauseOnBackground": false
}
}'>
<!-- Per-element override -->
<div hx-sse:connect="/stream" hx-config='{"sse": {"reconnect": false}}'>
| Option | Default (hx-sse:connect) | Default (hx-get) | Description |
|---|---|---|---|
reconnect | true | false | Auto-reconnect on stream end |
reconnectDelay | 500 | 500 | Initial reconnect delay (ms) |
reconnectMaxDelay | 60000 | 60000 | Maximum reconnect delay (ms) |
reconnectMaxAttempts | Infinity | Infinity | Maximum reconnection attempts |
reconnectJitter | 0.3 | 0.3 | Jitter factor (0-1) for delay randomization |
pauseOnBackground | true | false | Close the stream when the tab is backgrounded, reconnect when visible |
Reconnection Strategy
The extension uses exponential backoff with jitter:
- Formula:
delay = min(reconnectDelay × 2^(attempt-1), reconnectMaxDelay) - Jitter: Adds ±
reconnectJitterrandomization to avoid thundering herd - Last-Event-ID: Automatically sent on reconnection if the server provided message IDs
Events
htmx:before:sse:connection
Fired before a connection attempt (initial or reconnection). Set detail.connection.cancelled = true to prevent the connection.
For reconnections (detail.connection.attempt > 0), you can also modify detail.connection.delay to change the backoff delay.
document.body.addEventListener('htmx:before:sse:connection', function(evt) {
if (evt.detail.connection.attempt > 10) {
evt.detail.connection.cancelled = true;
}
});
detail.connection.attempt- attempt number (0= initial,> 0= reconnection)detail.connection.delay- the delay before connection (ms), modifiabledetail.connection.url- the SSE endpoint URLdetail.connection.lastEventId- the last event ID receiveddetail.connection.cancelled- set totrueto cancel
htmx:after:sse:connection
Fired after a successful connection (or reconnection) to the SSE stream.
detail.connection.attempt- attempt number (0= initial,> 0= reconnection)detail.connection.url- the SSE endpoint URLdetail.connection.status- the HTTP status codedetail.connection.lastEventId- the last event ID received
htmx:before:sse:message
Fired before each SSE message is processed. All fields are modifiable. Changes to data or event affect how the message is handled.
document.body.addEventListener('htmx:before:sse:message', function(evt) {
// Skip heartbeats
if (evt.detail.message.event === 'heartbeat') {
evt.detail.message.cancelled = true;
}
// Transform data before swap
evt.detail.message.data = sanitize(evt.detail.message.data);
});
detail.message.data- the message data (modifiable)detail.message.event- the event type (modifiable)detail.message.id- the message ID (if specified)detail.message.cancelled- set totrueto skip
htmx:after:sse:message
Fired after an SSE message has been processed.
detail.message- same shape ashtmx:before:sse:message
htmx:sse:error
Fired when a stream error occurs.
detail.error- the error object
htmx:sse:close
Fired when an SSE connection is closed.
detail.reason- why the connection was closed:"message"- closed byhx-sse:closematching a named event"removed"- the element was removed from the DOM"ended"- the stream ended naturally or reconnection was exhausted"cancelled"- the initial connection was cancelled viahtmx:before:sse:connection"cleanup"- closed during element cleanup (e.g., parent swap)