Polling is the simplest real-time technique there is, and for dashboards, build status and prices it is often all you need. Each tick is an ordinary request, so caching, auth and error handling work as usual.
jsKjs stops polling when the element is removed from the document, so swapping a panel out never leaves a timer running in the background. To pause, remove the element; to resume, insert fresh markup — it is wired automatically.
KJS / USD · every 2s
kj:after fired 0 times
Server clock · every 1s
ticker.php derives the price from the current time, so it moves without storing anything.
View the source for this demo
<div class="grid g2">
<div class="poll-card">
<p class="label" style="margin:0 0 10px">KJS / USD · every 2s</p>
<div id="tick-slot"><div id="ticker" kj-get="/api/ticker.php" kj-trigger="every 2s">Waiting for the first tick…</div></div>
<p class="muted" id="tick-meta" style="margin:8px 0 0;font-size:13.5px">kj:after fired 0 times</p>
</div>
<div class="poll-card">
<p class="label" style="margin:0 0 10px">Server clock · every 1s</p>
<div kj-get="/api/time.php" kj-trigger="every 1s">…</div>
</div>
</div>
<div class="row" style="margin-top:12px"><button type="button" id="tick-pause" aria-pressed="false">Pause ticker</button></div>let polls = 0;
// Delegated: keeps working after the ticker element is replaced
$(document).on('kj:after', '#ticker', () => $('#tick-meta').text('kj:after fired ' + (++polls) + ' times'));
$('#tick-pause').on('click', function () {
const pause = $(this).attr('aria-pressed') !== 'true';
if (pause) $('#ticker').remove(); // removed → its polling stops by itself
else $('#tick-slot').html('<div id="ticker" kj-get="/api/ticker.php" kj-trigger="every 2s">Resuming…</div>'); // auto-wired
$(this).attr('aria-pressed', String(pause)).text(pause ? 'Resume ticker' : 'Pause ticker');
});More examples
Build status
<section kj-get="/builds/42/status" kj-trigger="every 5s">
Building… (the server returns the finished badge when it's done)
</section>API
When to fire: any DOM event (with optional debounce), load (immediately), revealed (when scrolled into view, via IntersectionObserver), or every Ns (polling).
kj:before (before the request), kj:after (response received, before the swap; detail.html) and kj:error (detail.error.status) fire on the requesting element. kj:load fires on the target after the swap — or on its former parent when outerHTML replaced it. All bubble.
Migrating
hx-trigger="every 2s". htmx's 286 stop-polling status code is not supported — remove the element instead.Related features
Triggers & debouncing
Choose when requests fire with kj-trigger: any DOM event plus a debounce like keyup delay:400ms — live search and server-side inline validation without JavaScript..
Open →HyperRequest lifecycle events
Every declarative request emits bubbling lifecycle events — kj:before, kj:after, kj:error and kj:load — so one document-level listener can drive a page-wide progress bar or error toast..
Open →HyperLazy loading & infinite scroll
Load content when it scrolls into view with kj-trigger="revealed", or immediately with kj-trigger="load".
Open →