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.
<!-- 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) | ~10KB | ~14KB | ~40KB+ | ~15KB |
| Dependencies | 0 | 0 | Many | 0 |
| Learning curve | HTML only | HTML only | JS + JSX + tooling | HTML + JS |