htmx-ext-transport
v0.1.0
Published
htmx extension for routing requests through custom transport functions (WASM, IPC, mocks)
Maintainers
Readme
htmx-ext-transport
Route htmx requests through custom transport functions instead of window.fetch.
Requires htmx v4+. Not compatible with htmx 1.x or 2.x.
Use Cases
- WebAssembly — route requests to WASM modules in a SharedWorker
- Electron / Tauri — IPC to a native backend instead of HTTP
- Testing / Mocking — deterministic responses without a server
- Hybrid routing — some routes handled locally, some remotely, declared in HTML
Install
CDN
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx-ext-transport"></script>Or pinned:
<script src="https://unpkg.com/[email protected]/hx-transport.js"></script>npm
npm install htmx-ext-transportimport 'htmx.org';
import 'htmx-ext-transport';The extension is automatically active once loaded — no hx-ext attribute required.
Usage
1. Register a Transport
htmx.registerTransport("wasm", async (url, init) => {
const html = await sendToWasm(url, init);
return new Response(html, {
status: 200,
headers: { "Content-Type": "text/html" }
});
});A transport is a fetch-compatible function: (url: string, init: RequestInit) => Response | Promise<Response>.
2. Select It with hx-transport
<div hx-transport:inherited="wasm">
<button hx-get="/items" hx-target="#main">Load</button>
<form hx-post="/items/create">...</form>
</div>All requests within the div will use the wasm transport instead of window.fetch.
Switching Back to Default
Use hx-transport="fetch" to explicitly select the built-in default:
<div hx-transport:inherited="wasm">
<button hx-get="/items">Load via WASM</button>
<button hx-get="/api/upload" hx-transport="fetch">Upload to Server</button>
</div>API
htmx.registerTransport(name, fn)
Registers a named transport function.
| Parameter | Description |
|-----------|-------------|
| name | Transport name (case-insensitive). "fetch" is reserved. |
| fn | Fetch-compatible function: (url, init) => Response \| Promise<Response> |
Returns true if registered, false if the name is invalid/reserved or fn is not a function.
htmx.unregisterTransport(name)
Removes a previously registered transport.
| Parameter | Description |
|-----------|-------------|
| name | Transport name (case-insensitive) |
Returns true if removed, false if no matching transport was found.
How It Works
The extension hooks into htmx:config:request and:
- Reads the
hx-transportattribute (with standard htmx inheritance via:inherited) - Looks up the registered transport by name
- Sets
ctx.fetchto the transport function
The entire downstream pipeline runs normally — swap, OOB, partials, scripts, history, indicators, and other extensions all work as expected because the transport returns a standard Response.
Precedence
If ctx.fetch is already set (e.g., by another extension or a htmx:config:request listener), the transport extension will not override it.
Programmatic ctx.fetch Override
For simple cases you don't need this extension at all. You can listen to htmx:config:request and set ctx.fetch directly based on URL path or any other criteria:
document.body.addEventListener('htmx:config:request', (evt) => {
const ctx = evt.detail.ctx;
if (!ctx.request.action.startsWith('/wasm/')) return;
ctx.fetch = async (url, init) => new Response(await sendToWasm(url, init), {
status: 200,
headers: { 'Content-Type': 'text/html' }
});
});This approach is composable with hx-transport: a htmx:config:request listener that sets ctx.fetch takes precedence over the extension (since the extension skips if ctx.fetch is already set). So you can use hx-transport for declarative routing and programmatic ctx.fetch for overrides.
Notes
hx-transportfollows standard htmx inheritance rules — use:inheritedfor explicit inheritance- Transport lookup is case-insensitive (
"WASM"and"wasm"are equivalent) - Unknown transport names fall back to the default
fetchtransport - Transport functions receive the same
(url, init)arguments aswindow.fetch - Non-200 responses from transports go through the normal response pipeline (swap, error events, etc.)
- If a transport throws, htmx fires
htmx:errorand completes the request lifecycle normally
