---
title: "hx-ws"
description: "Enable bidirectional connections over WebSockets (WS)"
category: "Networking"
icon: "icon-[mdi--swap-horizontal]"
keywords: ["websockets", "ws", "real-time", "bidirectional", "socket"]
---

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@__VERSION__/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@__VERSION__/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>`](/docs#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` ([HCON](/docs#hx-config) or JSON):

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