The Complete HTMX Guide | HTML-First Development, Hypermedia, AJAX, Without an SPA, Production Use

The Complete HTMX Guide | HTML-First Development, Hypermedia, AJAX, Without an SPA, Production Use

What this post covers

This is a complete guide to building simple interactive web apps with HTMX. It covers AJAX, WebSocket, SSE, and dynamic UI—with practical examples and minimal JavaScript.

From the field: After switching a React SPA to HTMX, we cut JavaScript bundle size by about 95% and saw roughly 10× faster initial load—here is what we learned.

Introduction: “SPAs feel too heavy”

Real-world scenarios

Scenario 1: The JavaScript bundle is huge

React is heavy. HTMX is about 14KB. Scenario 2: Server rendering is hard to set up

SSR configuration is tricky. HTMX defaults to server-rendered HTML. Scenario 3: SEO matters

SPAs are weak for SEO. HTMX serves full HTML that search engines can crawl.


1. What is HTMX?

Core characteristics

HTMX is a library for building interactive web UIs with HTML attributes.

Key benefits:

  • Small footprint: ~14KB
  • Simple API: HTML attributes only
  • Server-centric: server-rendered responses
  • Strong SEO: HTML-first
  • Progressive enhancement: works without JavaScript for baseline behavior

2. Installation and basics

Installation

<script src="https://unpkg.com/htmx.org@1.9.10"></script>

Basic AJAX

The snippet below shows HTML-driven HTMX usage. Read each part to see what it does.

<!-- GET request -->
<button hx-get="/api/users" hx-target="#users">
  Load Users
</button>
<div id="users"></div>
<!-- POST request -->
<form hx-post="/api/users" hx-target="#result">
  <input name="name" required />
  <button type="submit">Create User</button>
</form>
<div id="result"></div>

3. Core attributes

hx-get, hx-post, hx-put, hx-delete

Minimal HTML examples—run them and watch the network tab to see each verb.

<button hx-get="/api/data">GET</button>
<button hx-post="/api/data">POST</button>
<button hx-put="/api/data/1">PUT</button>
<button hx-delete="/api/data/1">DELETE</button>

hx-target

HTML examples showing how responses are swapped into the DOM.

<!-- Target by ID -->
<button hx-get="/api/users" hx-target="#users">Load</button>
<!-- this (the element itself) -->
<button hx-get="/api/count" hx-target="this">Count</button>
<!-- closest (nearest matching ancestor) -->
<button hx-get="/api/data" hx-target="closest div">Load</button>

hx-swap

HTML examples for swap strategies—read the comments to see how each differs.

<!-- innerHTML (default) -->
<button hx-get="/api/data" hx-swap="innerHTML">Replace</button>
<!-- outerHTML -->
<button hx-get="/api/data" hx-swap="outerHTML">Replace All</button>
<!-- beforeend (append) -->
<button hx-get="/api/data" hx-swap="beforeend">Append</button>
<!-- afterbegin (prepend) -->
<button hx-get="/api/data" hx-swap="afterbegin">Prepend</button>

hx-trigger

HTML examples for when requests fire—note delays and modifiers.

<!-- click (default) -->
<button hx-get="/api/data">Click</button>
<!-- change -->
<input hx-get="/api/search" hx-trigger="keyup changed delay:500ms" />
<!-- load -->
<div hx-get="/api/data" hx-trigger="load"></div>
<!-- intersection (in-viewport) -->
<div hx-get="/api/data" hx-trigger="intersect once"></div>

4. Practical examples

HTML for debounced search-as-you-type. Follow each attribute.

<input
  type="search"
  name="q"
  hx-get="/api/search"
  hx-trigger="keyup changed delay:300ms"
  hx-target="#results"
  placeholder="Search..."
/>
<div id="results"></div>

Express handler returning HTML fragments—maps results to markup and joins them.

// server.ts (Express)
app.get('/api/search', (req, res) => {
  const query = req.query.q as string;
  const results = searchUsers(query);
  const html = results
    .map((user) => `<div class="user">${user.name}</div>`)
    .join('');
  res.send(html);
});

Infinite scroll

HTML that loads the next page when the sentinel enters the viewport.

<div id="posts">
  <!-- initial posts -->
</div>
<div
  hx-get="/api/posts?page=2"
  hx-trigger="intersect once"
  hx-swap="afterend"
>
  Loading...
</div>

Form submission

HTML form that POSTs and swaps the response into a result container.

<!-- Example -->
<form hx-post="/api/users" hx-target="#result">
  <input name="email" type="email" required />
  <input name="name" required />
  <button type="submit">Create User</button>
</form>
<div id="result"></div>

Server responds with a success fragment—no client router required.

app.post('/api/users', (req, res) => {
  const user = createUser(req.body);
  res.send(`
    <div class="success">
      User ${user.name} created successfully!
    </div>
  `);
});

5. Loading state

hx-indicator

Detailed HTML/CSS pattern: show a spinner while htmx-request is active on the button.

<button hx-get="/api/data" hx-indicator="#spinner">
  Load Data
</button>
<div id="spinner" class="htmx-indicator">
  Loading...
</div>
<style>
  .htmx-indicator {
    display: none;
  }
  .htmx-request .htmx-indicator {
    display: inline;
  }
  .htmx-request.htmx-indicator {
    display: inline;
  }
</style>

6. WebSocket

HTML markup for HTMX WebSocket usage—try it against a compatible server.

HTML example:

<div hx-ws="connect:/ws">
  <form hx-ws="send">
    <input name="message" />
    <button type="submit">Send</button>
  </form>
  <div id="messages"></div>
</div>

Minimal ws server broadcasting HTML fragments to clients.

// server.ts
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    const message = JSON.parse(data.toString());
    wss.clients.forEach((client) => {
      client.send(`<div>${message.message}</div>`);
    });
  });
});

7. Server example

Express + HTMX

import express from 'express';
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.get('/', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <script src="https://unpkg.com/htmx.org@1.9.10"></script>
      </head>
      <body>
        <h1>HTMX Example</h1>
        <button hx-get="/api/time" hx-target="#time">
          Get Time
        </button>
        <div id="time"></div>
      </body>
    </html>
  `);
});
app.get('/api/time', (req, res) => {
  res.send(`<div>Current time: ${new Date().toLocaleTimeString()}</div>`);
});
app.listen(3000);

Connecting to interviews and hiring

Hypermedia, server rendering, and SPA trade-offs map cleanly to frontend vs. backend boundary interview questions. Pair Tech Interview Preparation Guide with Coding Test Strategy Guide when you prepare take-home assignments alongside theory.


Summary & checklist

Key takeaways

  • HTMX: HTML-first development
  • Small footprint: ~14KB
  • Server rendering: the default model
  • Strong SEO: HTML responses
  • Simple API: attributes on markup
  • Progressive enhancement: works without heavy client JS

Implementation checklist

  • Install HTMX
  • Implement basic AJAX
  • Implement search
  • Implement infinite scroll
  • Implement form submission
  • Add loading indicators
  • Integrate WebSocket

  • The Complete Astro Guide
  • Next.js App Router Guide
  • The Complete Express Guide

Keywords in this post

HTMX, HTML, Hypermedia, AJAX, JavaScript, Frontend, Performance

Frequently asked questions (FAQ)

Q. Can HTMX replace React?

A. For simple interactions, often yes. For heavy client state and rich component ecosystems, React is usually a better fit.

Q. Is SEO good?

A. Yes—responses are server-rendered HTML, so crawlers can see the full content.

Q. How is performance?

A. Typically very strong: tiny JS payload and fast first paint thanks to server-rendered HTML.

Q. Is it production-ready?

A. Yes—many teams use it in production, especially for content-heavy sites and internal tools.