# WebSockets
The WebSocket extension enables real-time, bidirectional communication with
[WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications)
servers directly from HTML. It manages connections with automatic reconnection,
connection sharing, and seamless integration with htmx's swap and event model.
## Installing
```html
<script src="https://cdn.jsdelivr.net/npm/htmx.org@next/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@next/dist/ext/hx-ws.js"></script>
```
## Usage
Use these attributes to configure WebSocket behavior:
| Attribute | Description |
|-----------|-------------|
| `hx-ws:connect="<url>"` | Establishes a WebSocket connection to the specified URL |
| `hx-ws:send` | Sends form data or `hx-vals` to the WebSocket on trigger |
| `hx-ws:send="<url>"` | Like `hx-ws:send` but creates its own connection to the URL |
**JSX-Compatible Variants:** For frameworks that don't support colons in attribute names, use hyphen variants: `hx-ws-connect` and `hx-ws-send`.
### Basic Example
```html
<div hx-ws:connect="/chatroom" hx-target="#messages" hx-swap="beforeend">
<div id="messages"></div>
<form hx-ws:send>
<input name="message" placeholder="Type a message...">
<button type="submit">Send</button>
</form>
</div>
```
This example:
1. Establishes a WebSocket connection to `/chatroom` when the page loads
2. Appends incoming HTML messages to `#messages`
3. Sends form data as JSON when the form is submitted
## URL Normalization
WebSocket URLs are automatically normalized:
| Input | Output (on HTTPS page) |
|-------|------------------------|
| `/ws/chat` | `wss://example.com/ws/chat` |
| `ws://localhost:8080/ws` | `ws://localhost:8080/ws` |
| `https://api.example.com/ws` | `wss://api.example.com/ws` |
| `//cdn.example.com/ws` | `wss://cdn.example.com/ws` |
This means you can use simple relative paths in most cases, and the extension will construct the correct WebSocket URL.
## Receiving Messages
### JSON Message Format
Messages from the server should be JSON objects with a `content` field:
```json
{
"content": "<div class='notification'>New message!</div>",
"target": "#notifications",
"swap": "beforeend",
"HX-Request-ID": "abc-123"
}
```
| Field | Default | Description |
|-------|---------|-------------|
| `content` | | HTML content to swap into the target |
| `target` | Element's `hx-target` | CSS selector for target element |
| `swap` | Element's `hx-swap` | Swap strategy (innerHTML, beforeend, etc.) |
| `HX-Request-ID` | | Matches response to original request |
**Minimal example:**
```json
{"content": "<div>Hello World</div>"}
```
### Data-Only Messages
JSON messages without a `content` field are not swapped. Use `htmx:before:ws:message` to handle them:
```javascript
document.addEventListener('htmx:before:ws:message', (e) => {
if (e.detail.message.json?.type === 'notification') {
showNotification(e.detail.message.json.text);
}
});
```
### Raw HTML Messages
If the server sends a plain string (not JSON), it's treated as raw HTML:
```
<div class="alert">Server restarting in 5 minutes</div>
```
If the connection element has an `hx-target`, the HTML is swapped into that target. Without an `hx-target`, `swap:none` is used - but [`<hx-partial>`](https://four.htmx.org/docs/core-concepts/multi-target-updates#partials-hx-partial) elements in the message can still target their own destinations:
```
<hx-partial hx-target="#alerts" hx-swap="beforeend"><div class="alert">New alert</div></hx-partial>
```
### Request-Response Matching
Each sent message includes a unique `HX-Request-ID` header. When the server echoes it back in the response, the content is routed to the element that sent the request (not the connection element):
```html
<form hx-ws:send hx-target="#result">
<input name="query" value="hello">
<button>Search</button>
</form>
<div id="result"></div>
```
The form sends `{"headers": {"HX-Request-ID": "abc-123", ...}, "body": {"query": "hello"}}`. The server responds with:
```json
{"content": "<p>Results for 'hello'</p>", "HX-Request-ID": "abc-123"}
```
Because `HX-Request-ID` matches, the content is swapped into `#result` (the form's `hx-target`), not the connection element.
## Sending Messages
When an element with `hx-ws:send` is triggered, the extension sends a structured JSON message with separate `headers` and `body`:
```json
{
"headers": {
"HX-Request": "true",
"HX-Request-ID": "550e8400-e29b-41d4-a716-446655440000",
"HX-Source": "form#chat-form",
"HX-Target": "div#messages",
"HX-Current-URL": "https://example.com/chat"
},
"body": {
"message": "Hello!",
"tags": ["urgent", "public"]
}
}
```
**Headers** (from htmx core, same as HTTP requests):
| Header | Description |
|--------|-------------|
| `HX-Request` | Always `"true"` |
| `HX-Request-ID` | Unique ID for request/response matching |
| `HX-Source` | Source element identifier (`tag#id` format) |
| `HX-Target` | Target element identifier (only if `hx-target` is set) |
| `HX-Current-URL` | Current page URL |
**Body** contains form data and `hx-vals`. Multi-value fields (like checkboxes or multi-selects) are preserved as arrays.
### Forms
```html
<form hx-ws:send>
<input name="username">
<input name="message">
<button type="submit">Send</button>
</form>
```
### Buttons with hx-vals
```html
<button hx-ws:send hx-vals='{"action": "increment"}'>+1</button>
```
### Modifying Messages Before Send
Use the `htmx:before:ws:request` event to modify or cancel messages:
```javascript
document.addEventListener('htmx:before:ws:request', (e) => {
e.detail.headers['Authorization'] = 'Bearer ' + getToken();
// Or cancel the send
if (!isValid(e.detail.body)) {
e.preventDefault();
}
});
```
## Configuration
Configure the extension globally via `htmx.config.ws` or per-element via `hx-config`:
```javascript
htmx.config.ws = {
reconnect: true, // Enable auto-reconnect (default: true)
reconnectDelay: 500, // Initial reconnect delay in ms (default: 500)
reconnectMaxDelay: 60000, // Maximum reconnect delay in ms (default: 60000)
reconnectMaxAttempts: Infinity,// Maximum reconnect attempts (default: Infinity)
reconnectJitter: 0.3, // Jitter factor 0-1 for randomizing delays (default: 0.3)
pauseOnBackground: true, // Pause connection when tab is backgrounded (default: true)
pendingRequestTTL: 30000 // TTL for pending requests in ms (default: 30000)
};
```
```html
<!-- Per-element override -->
<div hx-ws:connect="/ws" hx-config="ws.reconnectDelay:1s ws.reconnectMaxAttempts:5">
```
Delay values accept time strings: `"500ms"`, `"1s"`, `"2m"`, or raw milliseconds.
Per-element config is read from the element that creates the connection and stored on the connection object for its lifetime.
### Reconnection Strategy
The extension uses exponential backoff with optional jitter:
- **Base formula**: `delay = min(reconnectDelay × 2^(attempts-1), reconnectMaxDelay)`
- **Jitter**: Adds ±30% randomization to avoid thundering herd
- **Reset**: Attempts counter resets to 0 on successful connection
Example reconnection delays with defaults:
- Attempt 1: ~500ms
- Attempt 2: ~1000ms
- Attempt 3: ~2000ms
- Attempt 4: ~4000ms
- Attempt 5+: ~60000ms (capped)
### Connection Triggers
By default, connections are established immediately when the element is processed:
```html
<!-- Connects immediately when element appears (default) -->
<div hx-ws:connect="/ws">
<!-- Explicit load trigger - same behavior as no trigger -->
<div hx-ws:connect="/ws" hx-trigger="load">
<!-- Deferred connection - only connects when button is clicked -->
<div hx-ws:connect="/ws" hx-trigger="click from:#connect-btn">
```
Use `hx-trigger` when you want to **delay** connection establishment (e.g., wait for user action).
All standard `hx-trigger` modifiers are supported (`delay`, `throttle`, `once`, etc.).
## Events
### Connection Lifecycle
| Event | Cancelable | Detail | Description |
|-------|------------|--------|-------------|
| `htmx:before:ws:connection` | ✅ | `{connection}` | Before connection attempt (initial or reconnect) |
| `htmx:after:ws:connection` | ❌ | `{connection}` | After successful connection |
| `htmx:ws:close` | ❌ | `{connection, reason, code}` | When connection closes (see below) |
| `htmx:ws:error` | ❌ | `{url, error}` | On connection or send error |
The `htmx:ws:close` detail includes:
- `connection` - the connection object
- `reason` - why the connection closed: `"closed"` (WebSocket close event), `"removed"` (element removed from DOM), or `"cancelled"` (connection cancelled via `htmx:before:ws:connection` event)
- `code` - the WebSocket close code (e.g. `1000`), or `null` when removed via DOM cleanup
The `connection` detail is the actual internal connection object, which includes:
- `attempt`: `0` for initial connection, `> 0` for reconnections
- `url`: the WebSocket URL
- `config`: the resolved configuration for this connection
- `socket`: the WebSocket instance (available on `after` events; `null` before initial connection)
- `cancelled`: set to `true` in `before` events to cancel the connection
- `pendingRequests`: Map of in-flight request/response pairs
### Request Events
| Event | Cancelable | Detail | Description |
|-------|------------|--------|-------------|
| `htmx:before:ws:request` | ✅ | `{headers, body}` | Before sending (headers and body are modifiable) |
| `htmx:after:ws:request` | ❌ | `{headers, body}` | After message sent |
### Message Events
| Event | Cancelable | Detail | Description |
|-------|------------|--------|-------------|
| `htmx:before:ws:message` | ✅ | `{message: {text, json, cancelled}}` | Before processing any received message |
| `htmx:after:ws:message` | ❌ | `{message: {text, json}}` | After processing any received message |
`message.text` is the raw string. `message.json` is the parsed JSON object (or `null` for non-JSON). Set `message.cancelled = true` in the `before` event to prevent processing.
### Event Examples
**Cancel Connection Based on Condition:**
```javascript
document.addEventListener('htmx:before:ws:connection', (e) => {
if (e.detail.connection.attempt > 10) {
e.detail.connection.cancelled = true; // Stop reconnecting
}
});
```
**Handle Data-Only Messages:**
```javascript
document.addEventListener('htmx:before:ws:message', (e) => {
if (e.detail.message.json?.type === 'audio') {
playAudioNotification(e.detail.message.json.url);
}
});
```
**Log All WebSocket Activity:**
```javascript
document.addEventListener('htmx:after:ws:connection', (e) => {
console.log('Connected to', e.detail.connection.url);
});
document.addEventListener('htmx:ws:close', (e) => {
console.log('Disconnected from', e.detail.connection.url, 'reason:', e.detail.reason);
});
```
## Connection Management
### Connection Sharing
Multiple elements pointing to the same WebSocket URL share a single connection:
```html
<div hx-ws:connect="/notifications" id="notif-1">
<!-- Uses connection to /notifications -->
</div>
<div hx-ws:connect="/notifications" id="notif-2">
<!-- Shares the same connection -->
</div>
```
When all elements using a connection are removed from the DOM, the connection is automatically closed.
### Element Cleanup
When elements are removed (e.g., via htmx swap), the extension checks if any other element in the DOM still references the same WebSocket URL. If not, the connection is closed.
This happens automatically through htmx's element cleanup lifecycle.
## HTML Swapping
When a JSON message with a `content` field arrives, the extension uses htmx's swap API, which provides:
- All swap styles (`innerHTML`, `outerHTML`, `beforebegin`, `afterend`, `beforeend`, `afterbegin`, `delete`, `none`)
- Preserved elements (`hx-preserve`)
- Auto-focus handling
- Scroll handling
- Proper cleanup of removed elements
- `htmx.process()` called on newly inserted content
### Target Resolution
Target is determined in this order:
1. `target` field in the message
2. `hx-target` attribute on the element that sent the request (if `HX-Request-ID` matches)
3. `hx-target` attribute on the connection element
4. The connection element itself
### Swap Strategy
Swap strategy is determined in this order:
1. `swap` field in the message
2. `hx-swap` attribute on the connection element (or the element that sent the request)
3. `htmx.config.defaultSwap` (default: `innerHTML`)
## Examples
### Live Chat
```html
<div hx-ws:connect="/chat">
<div id="messages" hx-target="this" hx-swap="beforeend"></div>
<form hx-ws:send>
<input name="message" placeholder="Message..." autocomplete="off">
<button type="submit">Send</button>
</form>
</div>
```
Server sends:
```json
{"content": "<div class='message'><b>User:</b> Hello!</div>"}
```
### Real-Time Notifications
```html
<div hx-ws:connect="/notifications"
hx-target="#notification-list"
hx-swap="afterbegin">
<div id="notification-list"></div>
</div>
```
### Interactive Counter
```html
<div hx-ws:connect="/counter">
<div id="count" hx-target="this">0</div>
<button hx-ws:send hx-vals='{"action":"increment"}'>+</button>
<button hx-ws:send hx-vals='{"action":"decrement"}'>-</button>
</div>
```
### Multiple Widgets Sharing Connection
```html
<div hx-ws:connect="/dashboard">
<div id="cpu-usage">--</div>
<div id="memory-usage">--</div>
<div id="disk-usage">--</div>
</div>
```
Server sends targeted updates:
```json
{"content": "<span>45%</span>", "target": "#cpu-usage"}
{"content": "<span>2.3 GB</span>", "target": "#memory-usage"}
```
## Upgrading from htmx 2.x
### Attribute Changes
| Old (htmx 2.x) | New (htmx 4.x) | Notes |
|----------------|----------------|-------|
| `ws-connect="<url>"` | `hx-ws:connect="<url>"` | Or `hx-ws-connect` for JSX |
| `ws-send` | `hx-ws:send` | Or `hx-ws-send` for JSX |
The old `ws-connect` and `ws-send` attributes still work but emit a deprecation warning.
### Event Changes
| Old Event | New Event | Notes |
|-----------|-----------|-------|
| `htmx:wsConnecting` | (none) | Removed |
| `htmx:wsOpen` | `htmx:after:ws:connection` | Different detail structure |
| `htmx:wsClose` | `htmx:ws:close` | Now includes `code` and `reason` |
| `htmx:wsError` | `htmx:ws:error` | Similar |
| `htmx:wsBeforeMessage` | `htmx:before:ws:message` | Different detail structure |
| `htmx:wsAfterMessage` | `htmx:after:ws:message` | Different detail structure |
| `htmx:wsConfigSend` | `htmx:before:ws:request` | Modify `e.detail.headers` and `e.detail.body` |
| `htmx:wsBeforeSend` | `htmx:before:ws:request` | Combined into one event |
| `htmx:wsAfterSend` | `htmx:after:ws:request` | Similar |
### Configuration Changes
| Old | New |
|-----|-----|
| `htmx.config.wsReconnectDelay` | `htmx.config.ws.reconnectDelay` |
| `createWebSocket` option | Not supported (use events) |
| `wsBinaryType` option | Not supported |
Note: earlier htmx 4.0 alpha builds used `htmx.config.websockets`. This has been renamed to `htmx.config.ws` for consistency with the extension name and attribute prefix.
### Message Format Changes
**Send payload** is now a structured `{headers, body}` JSON object. Headers contain `HX-Request`, `HX-Request-ID`, `HX-Source`, `HX-Target`, `HX-Current-URL`. Body contains form data and `hx-vals`.
**Receive format** now expects JSON with `content`, `target`, `swap` fields instead of raw HTML or `hx-swap-oob`. Messages with a `content` field are auto-swapped; messages without `content` are data-only (handle via events). Use `HX-Request-ID` (instead of the old `request_id`) for request-response matching.
### Socket Wrapper Removed
The `socketWrapper` object is no longer exposed in events. Use the standard WebSocket events and the extension's event system instead.
Enable bidirectional real-time communication via WebSockets
The WebSocket extension enables real-time, bidirectional communication with
WebSocket
servers directly from HTML. It manages connections with automatic reconnection,
connection sharing, and seamless integration with htmx’s swap and event model.
If the server sends a plain string (not JSON), it’s treated as raw HTML:
<div class="alert">Server restarting in 5 minutes</div>
If the connection element has an hx-target, the HTML is swapped into that target. Without an hx-target, swap:none is used - but <hx-partial> elements in the message can still target their own destinations:
Each sent message includes a unique HX-Request-ID header. When the server echoes it back in the response, the content is routed to the element that sent the request (not the connection element):
By default, connections are established immediately when the element is processed:
<!-- Connects immediately when element appears (default) --><div hx-ws:connect="/ws"><!-- Explicit load trigger - same behavior as no trigger --><div hx-ws:connect="/ws" hx-trigger="load"><!-- Deferred connection - only connects when button is clicked --><div hx-ws:connect="/ws" hx-trigger="click from:#connect-btn">
Use hx-trigger when you want to delay connection establishment (e.g., wait for user action).
All standard hx-trigger modifiers are supported (delay, throttle, once, etc.).
reason - why the connection closed: "closed" (WebSocket close event), "removed" (element removed from DOM), or "cancelled" (connection cancelled via htmx:before:ws:connection event)
code - the WebSocket close code (e.g. 1000), or null when removed via DOM cleanup
The connection detail is the actual internal connection object, which includes:
attempt: 0 for initial connection, > 0 for reconnections
url: the WebSocket URL
config: the resolved configuration for this connection
socket: the WebSocket instance (available on after events; null before initial connection)
cancelled: set to true in before events to cancel the connection
pendingRequests: Map of in-flight request/response pairs
message.text is the raw string. message.json is the parsed JSON object (or null for non-JSON). Set message.cancelled = true in the before event to prevent processing.
When elements are removed (e.g., via htmx swap), the extension checks if any other element in the DOM still references the same WebSocket URL. If not, the connection is closed.
This happens automatically through htmx’s element cleanup lifecycle.
Note: earlier htmx 4.0 alpha builds used htmx.config.websockets. This has been renamed to htmx.config.ws for consistency with the extension name and attribute prefix.
Send payload is now a structured {headers, body} JSON object. Headers contain HX-Request, HX-Request-ID, HX-Source, HX-Target, HX-Current-URL. Body contains form data and hx-vals.
Receive format now expects JSON with content, target, swap fields instead of raw HTML or hx-swap-oob. Messages with a content field are auto-swapped; messages without content are data-only (handle via events). Use HX-Request-ID (instead of the old request_id) for request-response matching.