Hyper · htmx-style

Server-Sent Events & WebSockets

Point kj-sse-connect at an event stream and every named event can be swapped into the page — no JavaScript, with automatic reconnect.

Streams as triggers and swaps

An element with kj-sse-connect="/url" opens an EventSource. Inside it, kj-sse-swap="name" swaps the data of each name event into that element using the usual kj-swap styles, and kj-trigger="sse:name" turns an event into a request trigger.

WebSockets work the same way: kj-ws-connect opens the socket, a form with kj-ws-send sends its fields as JSON, and HTML messages from the server are swapped in by id, like out-of-band swaps. Connections close when the element is removed and reconnect with backoff if they drop.

The demo endpoint below sends five events and then closes, because shared hosting doesn't keep PHP requests open for long. A real stream would keep going.

Live · A live activity feed from /api/stream.php

Not connected.

View the source for this demo
HTML
<div class="row"><button type="button" id="sse-start">Connect</button><button type="button" id="sse-stop" disabled>Disconnect</button></div>
<div id="sse-box"></div>
<p class="muted" id="sse-status" aria-live="polite">Not connected.</p>
JavaScript
$('#sse-start').on('click', () => {
  $('#sse-box').html(
    '<div kj-sse-connect="/api/stream.php" kj-sse-close="done">' +
    '  <p class="muted" style="margin:8px 0 0">Events: <b kj-sse-swap="count">0</b> · <span kj-sse-swap="done">streaming…</span></p>' +
    '  <ul class="sse-log" kj-sse-swap="message" kj-swap="afterbegin"></ul>' +
    '</div>');
  $.process('#sse-box');
  $('#sse-status').text('Connecting…');
  $('#sse-start').prop('disabled', true); $('#sse-stop').prop('disabled', false);
});
$('#sse-stop').on('click', () => {
  $('#sse-box').empty();                       // removing the element closes the stream
  $('#sse-status').text('Disconnected.');
  $('#sse-start').prop('disabled', false); $('#sse-stop').prop('disabled', true);
});
$(document).on('kj:sseOpen', () => $('#sse-status').text('Connected — one event a second.'));
$(document).on('kj:sseClose', () => { $('#sse-status').text('Stream finished.'); $('#sse-start').prop('disabled', false); $('#sse-stop').prop('disabled', true); });

More examples

SSE, declaratively

HTML
<div kj-sse-connect="/events">
  <ul kj-sse-swap="message" kj-swap="beforeend"></ul>
  <div kj-get="/cart" kj-trigger="sse:cart-changed"></div>
</div>

A PHP event stream

PHP
<?php
header('Content-Type: text/event-stream');
while (true) {
  echo "event: message\n";
  echo "data: <li>" . date('H:i:s') . "</li>\n\n";
  flush(); sleep(1);
}

WebSocket chat

HTML
<div kj-ws-connect="wss://example.com/chat">
  <div id="messages"></div>
  <form kj-ws-send><input name="text"><button>Send</button></form>
</div>

API

kj-sse-connect="/stream" · kj-sse-swap="event" · kj-trigger="sse:event" · kj-ws-connect="/ws" · kj-ws-send

Server-Sent Events and WebSockets for Hyper.

Reference: jskjs.live.js →
kj-swap="innerHTML | outerHTML | beforebegin | afterbegin | beforeend | afterend | delete | none | textContent [swap:Ns settle:Ns scroll:top show:… focus-scroll:true transition:true ignoreTitle:true]"

How it goes in, with htmx's modifiers (View Transitions for transition:true).

Reference: kj-swap →
kj-trigger="event[filter] once changed delay:… throttle:… from:… target:… consume queue:…, load, revealed, intersect, every 2s"

What starts the request.

Reference: kj-trigger →

Migrating

Coming from htmxThe sse and ws extensions: sse-connect / sse-swap / ws-connect / ws-send become kj-sse-connect / kj-sse-swap / kj-ws-connect / kj-ws-send. No hx-ext needed — loading the add-on enables them.

Related features