Spec-Zone.ru › htmx

hx-sse

Stream HTML with text/event-stream (SSE)

The hx-sse extension lets one HTTP response stream many Server-Sent Events (SSE).

If you used sse in htmx 2.0, see migration notes.

Installing

<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/ext/hx-sse.min.js"></script>

Usage

Update an Element

Start with a typical htmx request using hx-get:

<button hx-get="/ping">
  Ping
</button>

Instead of text/html, respond with text/event-stream:

HTTP/1.1 200 OK
Content-Type: text/event-stream

data: Pong

This unnamed event has a data: field but no event: field.

Backend libraries
  • Python: FastAPI or sse-starlette
  • Go: go-sse
  • PHP: Laravel event streams

The event replaces the button’s content:

<button hx-get="/ping">
  Pong
</button>

htmx uses the same rules as with a text/html response:

  • hx-target="this"
  • hx-swap="innerHTML" (from htmx.config.defaultSwap)

Stream an Update

You can also stream HTML using multiple unnamed events:

HTTP/1.1 200 OK
Content-Type: text/event-stream

data: P

data: Po

data: Pon

data: Pong

The button changes as each event arrives:

Ping → P → Po → Pon → Pong

Choose the Swap

Use hx-swap and hx-target to choose how and where updates swap:

<button hx-post="/generate"
        hx-target="next output"
        hx-swap="beforeend">
  Generate
</button>

<!-- LLM tokens stream here -->
<output></output>

Each unnamed event contains one text chunk:

HTTP/1.1 200 OK
Content-Type: text/event-stream

data: Hello

data: , world

data: !

hx-swap="beforeend" accumulates them in <output>:

<output>Hello, world!</output>

You can also use:

  • hx-select to select content for the swap
  • hx-select-oob to select more elements to swap

Update Elements

Use a normal htmx request to update several elements:

<button hx-get="/events">Connect</button>

<div id="feed"></div>
<div id="status">Offline</div>

The server sends two extra swaps using hx-swap-oob and <hx-partial>:

HTTP/1.1 200 OK
Content-Type: text/event-stream

data: <div id="status" hx-swap-oob="true">Online</div>
data: <hx-partial hx-target="#feed"><p>New</p></hx-partial>

The page becomes:

<button hx-get="/events">Connect</button>

<div id="feed">
  <p>New</p>
</div>
<div id="status">Online</div>
Why wasn't the normal swap used?

After htmx extracts the extra swaps, the normal swap is empty:

(empty)

hx-swap-oob and <hx-partial> elements are extracted before the normal swap. By default, swapEmpty:false leaves the connection element unchanged.

The server can mix extra swaps with ordinary HTML:

HTTP/1.1 200 OK
Content-Type: text/event-stream

data: <p>New event</p>
data: <hx-partial hx-target="#status">Busy</hx-partial>

The paragraph follows hx-target and hx-swap on the request element. The partial updates #status.

To disable the request element’s swap, set hx-swap="none":

<button hx-get="/events" hx-swap="none">Connect</button>

hx-swap-oob and <hx-partial> swaps still run.

Persistent Connections

Use a persistent connection to keep receiving server updates.

Open Connections

Add hx-sse:connect to the element that receives them:

<div hx-sse:connect="/events"></div>

Use hx-trigger to connect after an event:

<button id="connect">Connect</button>

<div hx-sse:connect="/events"
     hx-trigger="click from:#connect">
</div>

All hx-trigger modifiers are supported.

Close Connections

Close a connection when a specific named event arrives:

<div hx-sse:connect="/progress" hx-sse:close="done"></div>

The server sends:

HTTP/1.1 200 OK
Content-Type: text/event-stream

event: done
data: Complete

Client handlers for the done event run before the connection closes.

Configure Connections

You can configure hx-sse in three places:

  • <meta name="htmx-config"> sets global defaults from HTML.

    <meta name="htmx-config"
          content="sse.reconnectDelay:1s sse.reconnectMaxAttempts:5">
  • htmx.config.sse sets global defaults from JavaScript.

    htmx.config.sse.reconnectDelay = '1s'
    htmx.config.sse.reconnectMaxAttempts = 5
  • hx-config overrides the defaults for one connection.

    <div hx-sse:connect="/events"
         hx-config="sse.reconnectMaxAttempts:2">
    </div>

These values are read when stream handling begins.

Replay Messages

Add id: to recover messages missed while disconnected:

HTTP/1.1 200 OK
Content-Type: text/event-stream

id: event-42
data: <p>New message</p>

The stream keeps the current event ID:

  • An event without id inherits the current ID.
  • An empty id: clears the ID.
  • An ID-only block ending with a blank line updates or clears the ID without dispatching a message.

On reconnect, hx-sse includes Last-Event-ID when the current ID is not empty:

Last-Event-ID: event-42

This tells the server where the client left off, so it can replay missed messages.

Server                          Client

event-42  ------------------->  received
event-43  --------X             disconnected
          <-------------------  Last-Event-ID: event-42
event-43  ------------------->  replayed

The server decides which messages to replay. hx-sse processes every event it receives and does not deduplicate replays.

Trigger Client Events

An SSE event field dispatches a DOM event instead of swapping its data:

HTTP/1.1 200 OK
Content-Type: text/event-stream

event: progress
data: 50
id: task-5

Handle it with hx-on:

<button hx-get="/progress"
        hx-on:progress="htmx.find('#progress').value = event.detail.data">
  Start
</button>

<progress id="progress" max="100" value="0"></progress>

The event bubbles from the request source and exposes:

event.detail = {
  data: '50',
  id: 'task-5'
}

A named event can also trigger another htmx request:

<div hx-get="/status" hx-trigger="progress from:body"></div>

Attributes

hx-get/hx-post/hx-put/…

The hx-sse extension enhances:

  • hx-get
  • hx-post
  • hx-put
  • hx-patch
  • hx-delete
  • hx-action with hx-method

When a response uses Content-Type: text/event-stream, htmx processes each SSE event as it arrives.

hx-sse:connect

Opens a persistent SSE connection with a GET request:

<div hx-sse:connect="/events"></div>

The connection uses:

  • hx-headers and hx-vals for the GET request
  • hx-target, hx-swap, hx-select, and hx-select-oob for each message

Defaults:

  • hx-trigger="load"
  • sse.reconnect:true
  • sse.pauseOnBackground:true
  • sse.releaseOn:immediate
  • swapEmpty:false for each message

hx-sse:close

Closes the connection after a matching named event:

<div hx-sse:connect="/events" hx-sse:close="done"></div>
HTTP/1.1 200 OK
Content-Type: text/event-stream

event: done
data: Complete

Headers

Accept

Advertises SSE support on every htmx request while the extension is loaded.

Accept: text/html, text/event-stream

A response is streamed when its Content-Type contains text/event-stream.

Last-Event-ID

Identifies the last received SSE event during reconnection.

Last-Event-ID: event-42

The extension sends this header while the current event ID is not empty. Events without id inherit that ID, while an empty id: removes the header from later reconnects.

The server decides how to replay later messages. The client does not deduplicate them.

Events

Event data is available on event.detail.

Connection events expose:

event.detail.connection = {
  url,
  config,
  lastEventId,
  attempt,
  status,
  cancelled
}

Message events expose:

event.detail = {
  connection,
  message: {
    data,
    event,
    id
  },
  waitUntil(), // before processing only
  cancelled    // before processing only
}

htmx:sse:before:connection

Fires before htmx starts the initial stream or schedules a reconnect.

document.addEventListener('htmx:sse:before:connection', event => {
  if (event.detail.connection.attempt > 5) event.preventDefault()
})

The initial HTTP response has already arrived. Cancel either way:

  • call event.preventDefault()
  • set event.detail.connection.cancelled to true

htmx:sse:after:connection

Fires after the initial response or a reconnect is ready to stream.

document.addEventListener('htmx:sse:after:connection', event => {
  console.log('Connected:', event.detail.connection.url)
})

connection.status contains the HTTP status.

htmx:sse:before:message

Fires before processing an SSE message.

document.addEventListener('htmx:sse:before:message', event => {
  let message = event.detail.message
  if (message.event === 'heartbeat') event.preventDefault()
  else message.data = sanitize(message.data)
})

Changing message.data changes the swap or named event data. Changing message.event changes whether the message swaps or dispatches a DOM event.

detail.waitUntil(promise) delays processing until asynchronous work finishes. Cancel with event.preventDefault() or detail.cancelled = true.

htmx:sse:after:message

Fires after htmx swaps or dispatches an SSE message.

document.addEventListener('htmx:sse:after:message', event => {
  console.log('Received:', event.detail.message.data)
})

htmx:sse:close

Fires when an SSE stream closes.

document.addEventListener('htmx:sse:close', event => {
  console.log('Closed:', event.detail.reason)
})

reason is one of:

  • message: hx-sse:close matched a named event
  • removed: the source element left the DOM
  • ended: the stream ended or exhausted its reconnect attempts
  • cancelled: the initial stream was cancelled
  • cleanup: htmx cleaned up the source element

htmx:sse:error

Fires when reading or reconnecting to an SSE stream fails.

document.addEventListener('htmx:sse:error', event => {
  console.error('SSE error:', event.detail.error)
})
  • connection: the failed connection, when available
  • url: the SSE URL when setup fails before a connection exists
  • error: the error value
  • status: the HTTP status for a failed reconnect response, when available

hx:release

A server-sent event that ends the request lifecycle early. Only useful with sse.releaseOn:end.

event: hx:release
data:

When the server sends this event, htmx hides indicators and re-enables elements immediately. The stream continues running in the background.

This is useful for LLM streaming where you want to:

  • Show a loading indicator until the model starts responding
  • Let the user interact with the page while tokens continue streaming
<button hx-post="/generate"
        hx-target="#output"
        hx-swap="beforeend"
        hx-config="sse.releaseOn:end"
        hx-indicator="#spinner">
  Generate
</button>

The server streams:

data: First token

event: hx:release
data:

data: more tokens...

The indicator hides after hx:release, but tokens keep appending.

Config

sse.reconnect

Control whether a closed stream reconnects automatically.

<meta name="htmx-config" content="sse.reconnect:false">

Defaults to true for hx-sse:connect and false for normal htmx requests.

sse.reconnectDelay

Set how long to wait before the first reconnect attempt.

<meta name="htmx-config" content="sse.reconnectDelay:1s">

Defaults to 500 milliseconds. Each failed attempt doubles the delay, and values may be milliseconds or time strings such as 500ms, 1s, and 2m.

The server can replace this value for the stream:

HTTP/1.1 200 OK
Content-Type: text/event-stream

retry: 2000
data: Reconnect after two seconds

sse.reconnectMaxDelay

Limit how long to wait between reconnect attempts.

<meta name="htmx-config" content="sse.reconnectMaxDelay:30s">

Defaults to 60000 milliseconds. Use milliseconds or a time string.

sse.reconnectMaxAttempts

Limit how many times a closed stream tries to reconnect.

<meta name="htmx-config" content="sse.reconnectMaxAttempts:5">

Defaults to Infinity.

sse.reconnectJitter

Spread reconnect attempts so many clients do not retry at once.

<meta name="htmx-config" content="sse.reconnectJitter:0">

Defaults to 0.3, which randomizes each delay by up to ±30%. Use 0 for exact delays.

sse.pauseOnBackground

Close the stream while the page is hidden and reconnect when it becomes visible.

<meta name="htmx-config" content="sse.pauseOnBackground:false">

Defaults to true for hx-sse:connect and false for normal htmx requests. Use event IDs and server-side replay to recover messages sent while disconnected.

sse.releaseOn

Control when the request lifecycle ends (indicators hide, elements re-enable).

<meta name="htmx-config" content="sse.releaseOn:first">

Values:

  • immediate: release when SSE takes over (after headers arrive)
  • first: release after the first message swaps
  • end: release when the stream closes

Defaults to immediate for hx-sse:connect and end for normal htmx requests.

With the default end, indicators stay visible and hx-disable keeps elements disabled until the stream closes. This works well for LLM streaming where you want to prevent duplicate submissions.

Use first if you want the UI to become interactive as soon as content starts arriving:

<button hx-post="/generate"
        hx-config="sse.releaseOn:first">
  Generate
</button>

The server can also release early by sending an hx:release event:

event: hx:release
data:

Migration

htmx 2.0

htmx 2.0 used EventSource and selected named messages with sse-swap:

<div sse-connect="/chat" sse-swap="message"></div>

htmx 4 uses a normal htmx request and swaps unnamed messages automatically:

<div hx-sse:connect="/chat"></div>
HTTP/1.1 200 OK
Content-Type: text/event-stream

data: <p>New message</p>

Named messages now dispatch DOM events instead of selecting a swap target:

HTTP/1.1 200 OK
Content-Type: text/event-stream

event: progress
data: 50
<div hx-sse:connect="/progress"
     hx-on:progress="updateProgress(event.detail.data)">
</div>

Trigger another request with the ordinary event name:

<!-- htmx 2 -->
<div hx-get="/status" hx-trigger="sse:progress"></div>

<!-- htmx 4 -->
<div hx-get="/status" hx-trigger="progress from:body"></div>

Attributes

These attributes changed:

htmx 2.x htmx 4.x Compatibility
sse-connect hx-sse:connect Works with a warning
sse-swap Unnamed messages swap automatically Removed; warns
sse-close hx-sse:close Works with a warning

Events

These events changed:

htmx 2.x htmx 4.x
htmx:sseOpen htmx:sse:after:connection
htmx:sseError htmx:sse:error
htmx:sseBeforeMessage htmx:sse:before:message
htmx:sseMessage htmx:sse:after:message
htmx:sseClose htmx:sse:close

htmx 4 uses fetch() and ReadableStream instead of EventSource. SSE responses can therefore use any htmx HTTP method, request values, and headers.

Beta to RC1

RC1 namespaces SSE lifecycle events:

Beta RC1
htmx:before:sse:connection htmx:sse:before:connection
htmx:after:sse:connection htmx:sse:after:connection
htmx:before:sse:message htmx:sse:before:message
htmx:after:sse:message htmx:sse:after:message

Message cancellation moved from detail.message.cancelled to detail.cancelled. Before-message hooks can delay processing with detail.waitUntil(promise).

Licensed under the Zero-Clause BSD License.
https://four.htmx.org/extensions/hx-sse/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API