# tutorials · htmx
HTMX tutorial 2026: build interactive UI without a JS framework.
HTMX lets you build interactive UI with attributes on plain HTML: `hx-get`, `hx-post`, `hx-swap`, `hx-target`. No React, no build step, no client-side router. This tutorial walks through the six attributes you will use daily, then wires HTMX to a real WordPress REST endpoint for a live-search demo.
The premise
HTMX turns the browser into a smarter HTML client. Instead of shipping JSON to a React app that renders HTML, you ship HTML from the server and let HTMX swap fragments into the DOM. The result: less code, faster page loads, no hydration cost.
HTMX is not a React replacement for a Figma-plugin dashboard. It is a React replacement for the 80% of web UI that is a form, a table, a modal, or a live search box.
Install: one script tag
<script src="https://unpkg.com/[email protected]" crossorigin="anonymous"></script>That is the whole install. Add it to the <head> of any HTML page (WordPress theme, static site, Rails app, WordPress block editor) and every element with an hx-* attribute becomes reactive.
Attribute 1: hx-get (load HTML on click)
<button hx-get="/fragments/latest-posts.html" hx-target="#posts">
Load latest posts
</button>
<div id="posts"></div>On click, HTMX fires a GET to /fragments/latest-posts.html and swaps the response HTML into #posts. The server returns rendered HTML (not JSON), which is exactly what a WordPress action endpoint or a Rails partial already does.
Attribute 2: hx-post (submit a form)
<form hx-post="/api/subscribe" hx-target="#result" hx-swap="innerHTML">
<input type="email" name="email" required>
<button type="submit">Subscribe</button>
</form>
<div id="result"></div>Form submits via AJAX to /api/subscribe. Server responds with an HTML snippet (a success message, or a form with errors). HTMX swaps it into #result. No JavaScript required on the client.
Attribute 3: hx-swap (control where HTML lands)
<!-- Replace the target inner HTML (default). -->
<div hx-get="/tick" hx-swap="innerHTML">Loading</div>
<!-- Replace the target element entirely. -->
<div hx-get="/tick" hx-swap="outerHTML">Loading</div>
<!-- Insert before/after target. -->
<div hx-get="/new-row" hx-swap="beforeend" hx-target="#table tbody">Add row</div>The hx-swap options are innerHTML (default), outerHTML, beforebegin, afterbegin, beforeend, afterend, and delete. Match the DOM insertion behaviour you want.
Attribute 4: hx-trigger (fire on something other than click)
<!-- Live search: fire on keyup after 300ms of inactivity. -->
<input type="search" name="q"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results">
<div id="results"></div>
<!-- Poll every 5 seconds. -->
<div hx-get="/status" hx-trigger="every 5s">Loading status</div>
<!-- Fire when element enters viewport. -->
<div hx-get="/lazy" hx-trigger="revealed">Loading below the fold</div>hx-trigger unlocks live search, polling, lazy load below the fold, and infinite scroll (hx-trigger="revealed" hx-swap="afterend"). Modifiers like changed and delay:300ms match what you would write in a React useEffect, without the effect.
Attribute 5: hx-target (send response somewhere else)
<!-- Click the button, response lands in #modal. -->
<button hx-get="/user/42/edit" hx-target="#modal" hx-swap="innerHTML">
Edit user
</button>
<div id="modal"></div>
<!-- CSS selectors work: closest, next, previous. -->
<tr>
<td>Row 1</td>
<td><button hx-delete="/row/1" hx-target="closest tr" hx-swap="outerHTML">
Delete row
</button></td>
</tr>closest, next, previous, and find are all valid target selectors. Combined with hx-swap="outerHTML", you get row-delete behaviour with three attributes and zero JavaScript.
Wire HTMX to a WordPress REST endpoint
The WordPress REST API returns JSON by default, but HTMX wants HTML. Two options: (1) register an endpoint that returns HTML, or (2) let HTMX call admin-ajax.php. Option 1 is cleaner.
<?php
// functions.php
add_action('rest_api_init', function() {
register_rest_route('wdb/v1', '/search', [
'methods' => 'GET',
'callback' => 'wdb_search_html',
'permission_callback' => '__return_true',
]);
});
function wdb_search_html($req) {
$q = sanitize_text_field($req->get_param('q'));
$posts = get_posts([
'post_type' => 'post',
's' => $q,
'numberposts' => 5,
]);
ob_start();
if (empty($posts)) {
echo '<p>No results.</p>';
} else {
echo '<ul>';
foreach ($posts as $p) {
printf('<li><a href="%s">%s</a></li>',
esc_url(get_permalink($p)),
esc_html($p->post_title)
);
}
echo '</ul>';
}
$html = ob_get_clean();
$resp = new WP_REST_Response($html);
$resp->header('Content-Type', 'text/html');
return $resp;
}Then on the frontend:
<input type="search"
name="q"
hx-get="/wp-json/wdb/v1/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"
placeholder="Search posts">
<div id="results"></div>That is a live-search box with autocomplete, no build step, no React, no bundle. Total client-side JavaScript: 14KB of HTMX. Total custom JS: zero.
When HTMX is the right answer
- WordPress site with a live search, filter, or newsletter modal.
- Rails, Laravel, Django, or Phoenix app with mostly server-rendered HTML.
- Admin dashboard where you already have a template engine.
- Marketing site that needs a bit of interactivity (modal, tabs, live count).
When to reach for React or Vue instead
- Complex client-side state (a Figma-style editor, a spreadsheet).
- Offline-first PWA with heavy IndexedDB usage.
- Real-time collaboration (multiplayer cursor, live document editing).
- You already have a React app and adding HTMX would fragment the stack.
