Hyper · htmx-style

Declarative requests

Put the request in the markup: <form kj-post="/api/notes.php" kj-target="#notes">. The server returns HTML, jsKjs puts it where you said. No handler code.

Hypermedia, the short version

Instead of fetching JSON and rendering it in JavaScript, let the server send back the finished HTML fragment. jsKjs reads five attributes — kj-get, kj-post, kj-put, kj-patch, kj-delete — and wires the request for you.

Smart defaults keep markup short: forms fire on submit and send their fields; inputs and selects fire on change and send their own name=value; everything else fires on click. Bodies are URL-encoded (forms with file inputs switch to multipart automatically), exactly what PHP, Rails, Django and Laravel expect.

Content added later — including content returned by the server, like the delete buttons in the notes below — is wired automatically by a MutationObserver.

Live · Notes app with no JavaScript

  • The server renders each note — and its delete button.

Endpoint: /api/notes.php renders the note with PHP and escapes it. Nothing is stored — refresh and the list resets.

View the source for this demo
HTML
<form id="note-form" class="row" kj-post="/api/notes.php" kj-target="#notes" kj-swap="afterbegin">
  <label for="note-text" class="sr-only">New note</label>
  <input id="note-text" name="text" type="text" maxlength="140" required placeholder="Write a note, press Enter" style="flex:1;min-width:200px">
  <button type="submit">Add note</button>
</form>
<ul id="notes" class="list-plain" aria-live="polite" style="margin-top:12px">
  <li id="note-seed" class="note-row"><span>The server renders each note — and its delete button.</span>
    <button type="button" kj-delete="/api/notes.php?id=seed" kj-target="#note-seed" kj-swap="outerHTML" aria-label="Delete note">✕</button></li>
</ul>
JavaScript
// The only JavaScript on this demo: clear the field once the server has answered
$('#note-form').on('kj:after', function () { this.reset(); });

Live · All five verbs, one echo server

The server's view of your request appears here.

View the source for this demo
HTML
<div class="row">
  <button type="button" kj-get="/api/echo.php" kj-vals='{"q":"jskjs"}' kj-target="#verbs-out">kj-get</button>
  <button type="button" kj-post="/api/echo.php" kj-vals='{"title":"New"}' kj-target="#verbs-out">kj-post</button>
  <button type="button" kj-put="/api/echo.php" kj-vals='{"title":"Replaced"}' kj-target="#verbs-out">kj-put</button>
  <button type="button" kj-patch="/api/echo.php" kj-vals='{"title":"Tweaked"}' kj-target="#verbs-out">kj-patch</button>
  <button type="button" kj-delete="/api/echo.php?id=7" kj-target="#verbs-out">kj-delete</button>
</div>
<div id="verbs-out" aria-live="polite" style="margin-top:12px"><p class="muted">The server's view of your request appears here.</p></div>

More examples

Typical hypermedia markup

HTML
<!-- Load a partial when clicked -->
<button kj-get="/partials/stats.html" kj-target="#stats">Refresh stats</button>

<!-- Inline edit: PATCH and replace the row with the server's version -->
<form kj-patch="/api/rows/7" kj-target="#row-7" kj-swap="outerHTML">…</form>

<!-- A checkbox that saves itself on change -->
<input type="checkbox" name="done" kj-put="/api/tasks/3/done">

What the server returns (PHP)

PHP
<?php // /api/notes.php — return HTML, escape everything
$text = htmlspecialchars($_POST['text'] ?? '', ENT_QUOTES, 'UTF-8');
$id = bin2hex(random_bytes(4));
echo "<li id='note-$id'>$text <button kj-delete='/api/notes.php?id=$id'
      kj-target='#note-$id' kj-swap='outerHTML'>✕</button></li>";

API

kj-<verb>="/url"

The URL to request with that HTTP verb. Forms send their fields; named inputs, selects and textareas send their own value. Bodies are URL-encoded (multipart when a form contains file inputs). Default triggers: forms → submit, inputs/selects → change, everything else → click.

Reference: kj-get / kj-post / kj-put / kj-patch / kj-delete →
kj-target="#selector"

Where the response HTML goes (default: the element itself).

Reference: kj-target →

Migrating

Coming from htmxRename hx- to kj-: hx-get → kj-get, hx-target → kj-target. Requests carry X-Requested-With: jsKjs instead of htmx's HX-Request header.

Related features