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.
Not connected.
View the source for this demo
<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>$('#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
<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
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
<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
Server-Sent Events and WebSockets for Hyper.
Reference: jskjs.live.js →How it goes in, with htmx's modifiers (View Transitions for transition:true).
What starts the request.
Reference: kj-trigger →Migrating
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
Polling
Keep a panel fresh with kj-trigger="every Ns".
Open →HyperTargets & swaps
Control exactly where server HTML lands with kj-target and how it is inserted with kj-swap — innerHTML, outerHTML, beforebegin, afterbegin, beforeend, afterend, textContent or none..
Open →HyperBoost & out-of-band swaps
Turn ordinary links and forms into AJAX with kj-boost, and update several parts of the page from one response with kj-swap-oob.
Open →