REST APIs meet declarative HTML
Like htmx, but your server returns JSON and xhtmlx renders it client-side.
Build dynamic UIs with nothing but HTML attributes.
Up to 678x faster re-renders than React — see benchmarks.
<!-- That's it. No JavaScript. --> <div xh-get="/api/users" xh-trigger="load" xh-target="#results"> <template> <div xh-each="users"> <p xh-text="name"></p> </div> </template> </div> <div id="results"></div>
{ "users": [ { "name": "Alice Chen" }, { "name": "Bob Smith" }, { "name": "Carol Diaz" } ] } ↓ xhtmlx renders ↓ <div id="results"> <p>Alice Chen</p> <p>Bob Smith</p> <p>Carol Diaz</p> </div>
Add HTML attributes to your elements. xhtmlx handles the fetch, template rendering, and DOM updates automatically.
Your server stays a clean REST API returning JSON. xhtmlx handles every verb — from fetching data to creating, updating, and deleting resources.
Every field in the JSON response is accessible via dot notation. Navigate parent chains with $parent, reach the top with $root, and track position with $index.
{ "user": { "name": "Alice Chen", "role": "admin", "team": { "name": "Engineering", "members": [ { "name": "Bob" }, { "name": "Carol" } ] } } }
<!-- Dot notation for nested fields --> <h1 xh-text="user.name"></h1> <!-- Renders: Alice Chen --> <span xh-text="user.team.name"></span> <!-- Renders: Engineering --> <!-- Inside xh-each, $parent accesses outer scope --> <div xh-each="user.team.members"> <span xh-text="name"></span> <!-- $index: 0, 1, ... --> <span xh-text="$parent.user.role"></span> <!-- Renders: admin --> <span xh-text="$root.user.name"></span> <!-- Renders: Alice Chen --> </div>
Three ways to define templates: load from a file, embed inline, or bind directly to the element. All support xhtmlx attributes for recursive composition.
<div xh-get="/api/user/1" xh-trigger="load" xh-template="/templates/user.html"> </div>
<div class="card"> <h2 xh-text="name"></h2> <p xh-text="email"></p> </div>
<template> tag inside the element. No extra file needed.<div xh-get="/api/user/1" xh-trigger="load"> <template> <div class="card"> <h2 xh-text="name"></h2> <p xh-text="email"></p> </div> </template> </div>
<span xh-get="/api/count" xh-trigger="load" xh-text="total"> </span> <img xh-get="/api/user/1" xh-trigger="load" xh-attr-src="avatar" xh-attr-alt="name">
Powerful enough for production apps. Simple enough to learn in an afternoon.
Control exactly where rendered content lands with xh-swap. Combine with xh-target to direct output to any element on the page.
Use xh-target="#other-element" to direct rendered output to a different element on the page.
Real-world patterns you can copy-paste. Every line is plain HTML — no JavaScript required.
<div xh-get="/api/users" xh-trigger="load" xh-target="#user-list"> <template> <div xh-each="users"> <div class="user-card"> <img xh-attr-src="avatar" xh-attr-alt="name"> <h3 xh-text="name"></h3> <p xh-text="email"></p> <span xh-if="is_admin" class="badge">Admin</span> </div> </div> </template> <span class="xh-indicator">Loading...</span> </div> <div id="user-list"></div>
{ "users": [ { "name": "Alice", "email": "alice@co.io", "avatar": "/img/alice.jpg", "is_admin": true }, { "name": "Bob", "email": "bob@co.io", "avatar": "/img/bob.jpg", "is_admin": false } ] }
<!-- Create a new user --> <form xh-post="/api/users" xh-target="#user-list" xh-swap="beforeend" xh-template="/templates/user-row.html"> <input name="name" placeholder="Name" required> <input name="email" type="email" placeholder="Email"> <button type="submit">Add User</button> </form> <!-- List existing users --> <div xh-get="/api/users" xh-trigger="load" xh-template="/templates/user-table.html" xh-target="#user-list"></div> <div id="user-list"></div>
<tr id="user-{{id}}"> <td xh-text="name"></td> <td xh-text="email"></td> <td> <button xh-put="/api/users/{{id}}" xh-vals='{"role": "admin"}' xh-target="#user-{{id}}" xh-swap="outerHTML" xh-template="/templates/user-row.html">Promote</button> <button xh-delete="/api/users/{{id}}" xh-target="#user-{{id}}" xh-swap="delete">Remove</button> </td> </tr>
<div xh-get="/api/users/1" xh-trigger="load" xh-template="/templates/user-profile.html"></div>
<div class="profile"> <h1 xh-text="name"></h1> <p xh-text="bio"></p> <!-- Nested: load this user's posts --> <div xh-get="/api/users/{{id}}/posts" xh-trigger="load"> <template> <div xh-each="posts"> <h3 xh-text="title"></h3> <small>by <span xh-text="$parent.name"></span></small> </div> </template> </div> </div>
<input type="search" name="q" placeholder="Search users..." xh-get="/api/search" xh-trigger="keyup changed delay:300ms" xh-target="#search-results" xh-indicator="#search-spinner"> <span id="search-spinner" class="xh-indicator">Searching...</span> <div id="search-results"> <!-- Results appear here --> </div> <!-- Auto-refresh every 30s --> <div xh-get="/api/stats" xh-trigger="every 30s"> <template> <span>Online: <b xh-text="count"></b></span> </template> </div>
<!-- Error boundary catches unhandled errors from children --> <div xh-error-boundary xh-error-template="/templates/error.html" xh-error-target="#dashboard-errors"> <div id="dashboard-errors"></div> <!-- Widget with status-specific error templates --> <div xh-get="/api/profile" xh-trigger="load" xh-template="/templates/profile.html" xh-error-template-404="/templates/not-found.html" xh-error-template-4xx="/templates/client-error.html" xh-error-template="/templates/error.html"></div> <!-- This widget has no error template --> <!-- If it fails, the boundary catches it --> <div xh-get="/api/stats" xh-trigger="load"> <template><span xh-text="total"></span></template> </div> </div>
<div class="error-box"> <h3>Error <span xh-text="status"></span> — <span xh-text="statusText"></span></h3> <p xh-text="body.message"></p> <ul xh-if="body.fields"> <li xh-each="body.fields"> <strong xh-text="name"></strong>: <span xh-text="message"></span> </li> </ul> </div>
These widgets use the actual xhtmlx library to make real HTTP requests to JSONPlaceholder. Open DevTools Network tab to see the requests fly.
Open Playground — write your own xhtmlx code with a built-in mock API
No build tools, no configuration, no package managers required. But they work too.
<!-- Add to your HTML --> <script src="https://unpkg.com/xhtmlx"></script> <!-- Or pin a specific version --> <script src="https://unpkg.com/xhtmlx@1.0.0"></script>
# Install npm install xhtmlx # Then add to your HTML <script src="node_modules/xhtmlx/xhtmlx.js"></script>
Different tools make different tradeoffs. Here is where xhtmlx sits in the landscape.
| Feature | xhtmlx | htmx | React | Alpine.js |
|---|---|---|---|---|
| Server returns | JSON | HTML | JSON | N/A |
| Build step required | No | No | Yes | No |
| Client-side rendering | Yes | No | Yes | Yes |
| External template files | Yes | N/A | JSX | No |
| Declarative HTML | Yes | Yes | No | Yes |
| REST API friendly | Yes | No | Yes | Yes |
| File size (gzipped) | ~17KB | ~14KB | ~46KB+ | ~15KB |
| Dependencies | 0 | 0 | Many | 0 |
| Learning curve | HTML only | HTML only | JS + JSX + tooling | HTML + JS |
xhtmlx patches only changed DOM bindings in place. No virtual DOM, no diffing, no reconciliation. When data changes, only the pixels that need to change, change.
| Scenario | xhtmlx | React 19 | Result |
|---|---|---|---|
| 1 text — changing data | 13.48M ops/s | 49.7K ops/s | xhtmlx 271x |
| 1 text — same data (noop) | 12.81M ops/s | 32.0K ops/s | xhtmlx 400x |
| 5 text — same data (noop) | 32.48M ops/s | 128.1K ops/s | xhtmlx 253x |
| 10 text — changing data | 1.45M ops/s | 8.1K ops/s | xhtmlx 178x |
| Card component — changing | 5.98M ops/s | 25.2K ops/s | xhtmlx 237x |
| Card component — same (noop) | 20.67M ops/s | 30.5K ops/s | xhtmlx 678x |
| Conditional — same (noop) | 16.11M ops/s | 44.9K ops/s | xhtmlx 359x |
| Profile widget — same (noop) | 14.82M ops/s | 22.9K ops/s | xhtmlx 647x |
xhtmlx's render() API patches only changed bindings in place — no virtual DOM
diff, no reconciliation, no tree walk. This is the real-world advantage for polling, WebSocket
streams, and reactive state updates.
| Scenario | xhtmlx | React 19 | Result |
|---|---|---|---|
| Single text binding | 1.41M ops/s | 25.4K ops/s | xhtmlx 56x |
| 5 text bindings | 1.47M ops/s | 55.7K ops/s | xhtmlx 26x |
| Conditional render | 1.52M ops/s | 17.0K ops/s | xhtmlx 89x |
| User profile card | 1.37M ops/s | 8.0K ops/s | xhtmlx 171x |
| List — 100 items | 1.50M ops/s | 815 ops/s | xhtmlx 1,836x |
| List — 500 items | 1.76M ops/s | 209 ops/s | xhtmlx 8,405x |
| List — 1,000 items | 1.94M ops/s | 95 ops/s | xhtmlx 20,493x |
performSwap auto-patches when the same template is re-rendered into the same target —
the DOM is never rebuilt, only changed bindings are updated. Combined with lazy fragment construction,
the full pipeline matches render() speed.
Benchmarks run in JSDOM with Jest. React uses flushSync for fair synchronous comparison.
Source: tests/benchmark/
· 3 runs averaged per measurement · React 19.2 · xhtmlx 0.4.1