Skip to content
· 7 min read · 0 views

HTMX: Build Dynamic Web Apps Without Writing JavaScript

Learn how HTMX enables dynamic web interactions using HTML attributes alone. No JavaScript frameworks needed — just HTML, CSS, and a backend.

// table of contents (22 sections)

The JavaScript fatigue is real. What if you could build dynamic, interactive web applications without managing complex state, learning new frameworks every year, or shipping megabytes of client-side code?

HTMX offers exactly that — a return to the original vision of the web as a hypermedia system. No React. No Vue. No build steps. Just HTML attributes that tell the browser: “When this happens, fetch HTML from the server and put it here.”


What is HTMX?

HTMX is a small (~14KB minified) JavaScript library that extends HTML with special attributes. These attributes let you trigger AJAX requests, CSS transitions, and WebSocket connections directly from HTML elements — no JavaScript required.

<button hx-get="/users" hx-target="#user-list">
  Load Users
</button>
<div id="user-list"></div>

When clicked, this button:

  1. Sends a GET request to /users
  2. Receives HTML fragments from the server
  3. Places the response into #user-list

That’s it. No fetch, no promises, no state management. The server returns HTML, and HTMX swaps it into the DOM.


Why HTMX Matters in 2026

The Problem with Modern Frontend

Modern Single Page Applications (SPAs) have introduced significant complexity:

IssueSPA ApproachHTMX Approach
StateClient manages state syncServer is source of truth
RoutingClient-side routerStandard URLs, form submissions
Data Fetchingfetch() + JSON + state updatesHTML fragments
Build ToolsWebpack, Vite, bundlingNone required
Bundle Size100KB - 500KB+~14KB

The SPA model made sense when browsers were slow and server roundtrips were expensive. Today, with HTTP/2 multiplexing and edge caching, the calculus has changed.

Hypermedia-Driven Applications

HTMX promotes Hypermedia-Driven Applications (HDAs) — where HTML is the communication medium, not JSON. This isn’t regression; it’s a return to RESTful principles with modern capabilities.

Related: The Modern Frontend Stack of 2026 explores how Astro embraces similar principles with islands architecture.


Core HTMX Attributes

Triggering Requests

HTMX provides attributes for all HTTP methods:

<!-- GET requests -->
<button hx-get="/api/data">Fetch Data</button>

<!-- POST requests (forms, actions) -->
<form hx-post="/api/users" hx-target="#result">
  <input name="username" />
  <button type="submit">Create User</button>
</form>

<!-- PUT and DELETE -->
<button hx-put="/api/users/1">Update</button>
<button hx-delete="/api/users/1">Delete</button>

Trigger Modifiers

Control when requests fire:

<!-- Trigger on events other than click -->
<input hx-get="/search" hx-trigger="keyup changed delay:500ms" />

<!-- Trigger on load -->
<div hx-get="/notifications" hx-trigger="load"></div>

<!-- Trigger every 30 seconds -->
<div hx-get="/live-data" hx-trigger="every 30s"></div>

<!-- Trigger on custom events -->
<div hx-get="/refresh" hx-trigger="refresh-data from:body"></div>

Target and Swap

Control where content goes and how it appears:

<!-- Target specific element -->
<button hx-get="/modal" hx-target="#modal-container">
  Open Modal
</button>

<!-- Swap strategies -->
<div hx-get="/comments" 
     hx-swap="innerHTML">    <!-- default: replace content -->
<div hx-get="/comments" 
     hx-swap="outerHTML">    <!-- replace entire element -->
<div hx-get="/comments" 
     hx-swap="beforebegin">  <!-- insert before -->
<div hx-get="/comments" 
     hx-swap="afterend">     <!-- insert after -->
<div hx-get="/comments" 
     hx-swap="delete">       <!-- remove element -->

<!-- Morph DOM smoothly -->
<div hx-get="/list" hx-swap="morph:innerHTML"></div>

Let’s build a live search feature — typically requiring React state, effects, and debouncing. With HTMX, it’s declarative HTML:

<!-- search.html -->
<div class="search-container">
  <input 
    type="text" 
    name="q"
    hx-get="/api/search" 
    hx-trigger="keyup changed delay:300ms, search" 
    hx-target="#results"
    hx-indicator="#spinner"
    placeholder="Search users..."
  />
  <span id="spinner" class="htmx-indicator">Searching...</span>
</div>

<div id="results"></div>

The backend (Go example):

// main.go
package main

import (
    "html/template"
    "net/http"
    "strings"
)

var users = []string{
    "Alice Johnson", "Bob Smith", "Charlie Brown",
    "Diana Prince", "Eve Williams", "Frank Miller",
}

func searchHandler(w http.ResponseWriter, r *http.Request) {
    query := strings.ToLower(r.URL.Query().Get("q"))
    
    var results []string
    for _, user := range users {
        if strings.Contains(strings.ToLower(user), query) {
            results = append(results, user)
        }
    }
    
    // Return HTML fragment
    tmpl := `<ul class="search-results">
        {{range .}}
        <li class="result-item">{{.}}</li>
        {{end}}
    </ul>`
    
    t := template.Must(template.New("results").Parse(tmpl))
    t.Execute(w, results)
}

func main() {
    http.HandleFunc("/api/search", searchHandler)
    http.ListenAndServe(":8080", nil)
}

No JavaScript. No state management. The server is the source of truth.


Form Validation with HTMX

Server-side validation becomes trivial with HTMX:

<form hx-post="/api/register" hx-target="#form-result">
  <div class="form-group">
    <label>Email</label>
    <input 
      name="email" 
      type="email"
      hx-post="/api/validate-email"
      hx-trigger="change"
      hx-target="next .error"
    />
    <span class="error"></span>
  </div>
  
  <div class="form-group">
    <label>Username</label>
    <input 
      name="username"
      hx-post="/api/validate-username"
      hx-trigger="change"
      hx-target="next .error"
    />
    <span class="error"></span>
  </div>
  
  <button type="submit">Register</button>
</form>

<div id="form-result"></div>

Backend returns HTML fragments for errors:

// Returns: <span class="error text-red">Email already registered</span>
// or: <span class="error text-green">✓ Available</span>

Infinite Scroll

Loading more content as the user scrolls:

<div id="posts-container">
  <!-- Initial posts -->
  <div class="post">Post 1</div>
  <div class="post">Post 2</div>
  
  <!-- Trigger next page load -->
  <div 
    hx-get="/api/posts?page=2"
    hx-trigger="revealed"
    hx-swap="outerHTML"
  >
    Loading more...
  </div>
</div>

Each page returns posts plus the next trigger:

<div class="post">Post 3</div>
<div class="post">Post 4</div>
<div 
  hx-get="/api/posts?page=3"
  hx-trigger="revealed"
  hx-swap="outerHTML"
>
  Loading more...
</div>

WebSocket and SSE

Real-time updates without JavaScript:

<!-- Server-Sent Events -->
<div hx-ext="sse" sse-connect="/api/events">
  <div sse-swap="message" hx-swap="innerHTML">
    Waiting for updates...
  </div>
</div>

<!-- WebSocket -->
<div 
  hx-ext="ws" 
  ws-connect="/api/chat"
>
  <div id="messages" ws-swap="message"></div>
  
  <form ws-send>
    <input name="message" />
    <button>Send</button>
  </form>
</div>

Related: For real-time architectures with Flutter, see Building a Flutter AI Voice Assistant.


Loading States and UX

HTMX provides built-in indicators:

<style>
  .htmx-request .htmx-indicator { display: inline; }
  .htmx-request .normal-state { display: none; }
</style>

<button hx-get="/slow-endpoint">
  <span class="normal-state">Submit</span>
  <span class="htmx-indicator">Loading...</span>
</button>

Disable elements during requests:

<button 
  hx-get="/api/data"
  hx-disabled-elt="this"
>
  Load Data
</button>

Error Handling

Handle HTTP errors gracefully:

<div 
  hx-get="/api/data"
  hx-target="#content"
  hx-on::htmx:beforeRequest="console.log('Loading...')"
  hx-on::htmx:responseError="alert('Request failed')"
>
  Load Data
</div>

Or use htmx:afterRequest events:

<div 
  hx-get="/api/data"
  hx-target="#content"
  _="on htmx:responseError(alert 'Error: ' + event.detail.xhr.status)"
>
  Load Data
</div>

The _ attribute uses hyperscript for simple client-side logic.


When to Use HTMX

HTMX Excels At:

  • Content-driven sites — blogs, dashboards, admin panels
  • Form-heavy applications — CRUD interfaces, wizards
  • Teams with strong backend skills — leverage existing expertise
  • Progressive enhancement — works without JavaScript
  • Rapid prototyping — no build step, immediate feedback

HTMX May Not Fit:

  • Highly interactive UIs — games, drag-and-drop editors
  • Offline-first apps — need service workers and local state
  • Complex client-side animations — Three.js, complex SVG

For offline-first architecture, see Local-First Software Architecture.


Backend Agnostic

HTMX works with any backend that returns HTML:

LanguageFramework
GoGin, Chi, Fiber
PythonDjango, Flask, FastAPI
RubyRails, Sinatra
Node.jsExpress, Fastify
PHPLaravel, Symfony
RustAxum, Actix

Related: Building RESTful APIs with Go and Chi pairs well with HTMX frontend.


Security Considerations

HTMX doesn’t introduce new vulnerabilities, but remember:

  1. CSRF Protection — Include CSRF tokens in forms
  2. Input Validation — Always validate server-side
  3. Rate Limiting — HTMX triggers can be rapid
  4. CORS — Only needed for cross-origin requests
<form hx-post="/api/action">
  <input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
  <button>Submit</button>
</form>

Performance Comparison

Real-world metrics from a dashboard application:

MetricReact SPAHTMX
Initial Load340KB JS + 45KB CSS14KB HTMX + 12KB CSS
Time to Interactive2.1s0.4s
Server Requests1 initial + API callsHTML fragments
Memory Usage45MB8MB

The HTMX version loads 20x less JavaScript and becomes interactive 5x faster.


Getting Started

  1. Add HTMX via CDN:
<script src="https://unpkg.com/htmx.org@latest"></script>
  1. Add attributes to your HTML elements

  2. Return HTML fragments from your backend

  3. Enjoy simplicity — no build step, no state management

For a deeper dive into the philosophy, read HTMX Essays — they challenge modern frontend assumptions with historical context.


Conclusion

HTMX represents a paradigm shift back to hypermedia principles. By letting HTML describe its own behavior, we eliminate the impedance mismatch between server and client. The server becomes the source of truth, state lives where it belongs, and complexity drops dramatically.

In 2026, with edge computing and HTTP/2 making server roundtrips faster than ever, HTMX offers a compelling alternative to JavaScript-heavy SPAs. Not every application needs React. Many applications just need HTML that knows how to update itself.

Less JavaScript, more web. 🚀

You might also like

Enjoyed This Post?

Want to discuss the topic, have questions, or looking to collaborate on something similar? Drop a comment below or reach out directly.

Discussion