htmx — Building Dynamic Web Apps Without JavaScript Frameworks

Posted on: 4/25/2026 5:15:45 PM

~14KB Gzipped size — 20x smaller than React
47K+ GitHub Stars
0 Dependencies — zero external libraries
0 Build steps — no bundler or transpiler needed

1. What is htmx?

htmx is an ultra-lightweight JavaScript library (~14KB gzipped) that lets you build dynamic web applications directly from HTML — no JavaScript to write, no build step, no virtual DOM. Instead of loading a heavy framework and rendering UI on the client, htmx lets the server return HTML fragments and swaps them into the right place in the DOM.

The core philosophy is simple: HTML is already a hypermedia, but it's limited by the fact that only <a> and <form> can make HTTP requests, only click and submit can trigger them, and only GET/POST are available. htmx removes all of these constraints.

Hypermedia-Driven Application (HDA)

The HDA architecture is the opposite of SPAs: the server holds all logic and state, returning HTML fragments via AJAX. The client simply swaps HTML into the DOM — no JSON parsing, no state management, no client-side routing. This is exactly how the web was designed to work, just smoother.

2. Core Attributes — Everything Starts from HTML

htmx extends HTML with a simple yet powerful set of attributes. You don't need to write a single line of JavaScript — just add attributes to HTML elements.

hx-get / hx-post / hx-put / hx-delete

Send HTTP requests with any method from any element — not limited to <a> or <form>.

hx-trigger

Choose which event triggers the request: click, keyup, load, revealed, every 2s, or custom events.

hx-target

Specify which element receives the response HTML. Uses CSS selectors: #result, closest tr, next .panel.

hx-swap

How to swap the response into the target: innerHTML, outerHTML, beforeend, afterbegin, delete, none.

hx-indicator

Automatically show a loading indicator while a request is in flight — great UX with no loading state management.

hx-confirm

Show a confirmation dialog before sending the request — ideal for delete actions.

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

<span id="spinner" class="htmx-indicator">Loading...</span>
<div id="results"></div>

With just 5 lines of HTML, you get live search with 300ms debounce, a loading indicator, and auto-rendered results — no React, no useState, no useEffect.

Example: Infinite Scroll

<tr hx-get="/contacts?page=2"
    hx-trigger="revealed"
    hx-swap="afterend">
  <td>Loading More...</td>
</tr>

The revealed trigger fires when the element scrolls into the viewport — the server returns the next batch of rows along with a new trigger row for page 3. Complete infinite scroll without IntersectionObserver or scroll event handlers.

3. Hypermedia vs SPA Architecture

graph LR
    subgraph SPA["Traditional SPA"]
        C1[Client App
React/Vue] -->|"JSON API"| S1[REST/GraphQL Server] S1 -->|"JSON Data"| C1 C1 -->|"Client Router
State Manager
Virtual DOM"| U1[UI Render] end subgraph HDA["Hypermedia htmx"] C2[HTML + htmx
~14KB] -->|"HTTP Request"| S2[Server] S2 -->|"HTML Fragment"| C2 C2 -->|"DOM Swap"| U2[UI Update] end style SPA fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style HDA fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50 style C1 fill:#e94560,stroke:#fff,color:#fff style S1 fill:#2c3e50,stroke:#fff,color:#fff style U1 fill:#16213e,stroke:#fff,color:#fff style C2 fill:#4CAF50,stroke:#fff,color:#fff style S2 fill:#2c3e50,stroke:#fff,color:#fff style U2 fill:#16213e,stroke:#fff,color:#fff

SPA vs Hypermedia-Driven Application architecture comparison

Criteria SPA (React/Vue) htmx (Hypermedia)
Bundle size 150–500KB+ (framework + deps) ~14KB (htmx only)
Build tooling Webpack/Vite + transpiler + bundler None — just add a script tag
State management Redux/Pinia/Zustand + sync logic Server holds state — Single Source of Truth
Server response JSON → client parse → render HTML fragment → direct swap
SEO Requires complex SSR/SSG setup SEO-friendly by default (server-rendered)
Offline support Possible (Service Worker + cache) Limited — server-dependent
Best for Rich interactive apps, offline-first CRUD, dashboards, content-heavy sites
Learning curve High — JSX, hooks, state, routing, build Low — know HTML and you're good

4. htmx 4.0 — The Big Leap in 2026

htmx 4.0 (currently in beta, expected mid-2026) brings the most significant architectural changes since htmx's inception:

4.1. XHR to Fetch API Migration

The entire transport layer has been rewritten from XMLHttpRequest to the Fetch API. This isn't just an API swap — it unlocks an entirely new capability: streaming responses.

// htmx 4.0 leverages ReadableStream
// Server sends HTML fragments progressively → real-time UI updates
// No need to wait for the entire response to complete

<div hx-get="/dashboard/live"
     hx-trigger="load">
  <!-- Fragments stream in as the server processes each part -->
</div>

Streaming UI — The Holy Grail of Modern Web

With Fetch streaming, htmx 4.0 can receive and render HTML parts as the server sends them — similar to React Server Components streaming but without the complex framework. A dashboard with 10 widgets? Each widget renders as soon as its data is ready, no waiting for all 10 to complete.

4.2. Idiomorph — Smart DOM Merging

Previously an extension, htmx 4.0 makes Idiomorph the default. Instead of crude swaps (replacing all innerHTML), Idiomorph diffs old and new HTML and only updates nodes that actually changed — preserving focus state, scroll position, and CSS transitions.

graph TD
    A["Server Response
HTML Fragment"] --> B["Idiomorph Diff"] B --> C{"Node changed?"} C -->|"Yes"| D["Update that node"] C -->|"No"| E["Keep as-is"] D --> F["DOM Updated
Focus & Scroll preserved"] E --> F style A fill:#e94560,stroke:#fff,color:#fff style B fill:#2c3e50,stroke:#fff,color:#fff style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style D fill:#4CAF50,stroke:#fff,color:#fff style E fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50 style F fill:#16213e,stroke:#fff,color:#fff

Idiomorph diff algorithm — only updates changed DOM nodes

4.3. <hx-partial> — Multi-target Swap

The new <hx-partial> tag allows server responses to contain multiple fragments, each targeting a different element:

<!-- Server response with multiple partials -->
<hx-partial hx-target="#sidebar">
  <nav>Updated sidebar content</nav>
</hx-partial>

<hx-partial hx-target="#notification-count">
  <span class="badge">3</span>
</hx-partial>

<hx-partial hx-target="#main-content">
  <div>New main content here</div>
</hx-partial>

A single request can update the sidebar, notification badge, and main content simultaneously — completely replacing the old Out-of-Band Swaps with cleaner syntax.

4.4. Status-Specific Swapping

Handle HTTP errors directly in HTML, no JavaScript error handler needed:

<form hx-post="/api/orders"
      hx-target="#result"
      hx-status:404="#not-found"
      hx-status:422="#validation-errors"
      hx-status:5xx="#server-error">
  ...
</form>

<div id="not-found" style="display:none"></div>
<div id="validation-errors" style="display:none"></div>
<div id="server-error" style="display:none"></div>

4.5. Built-in View Transitions API

htmx 4.0 integrates the browser's View Transitions API by default — page transitions and element swaps come with smooth animations without writing manual CSS transitions.

4.6. Breaking Change: Prop Inheritance

Note When Migrating from htmx 2.x

Attributes like hx-target no longer automatically inherit from parent elements. You must use the explicit :inherited modifier: hx-target:inherited="#div". This change improves locality of behavior — code is easier to read since each element is self-contained.

5. Integrating htmx with ASP.NET Core

htmx works with any backend that can return HTML — but integrates particularly well with server-side rendering frameworks like ASP.NET Core Razor Pages/MVC.

5.1. Minimal API Endpoint Returning HTML Fragments

// Program.cs — ASP.NET Core Minimal API
app.MapGet("/search", async (string? q, AppDbContext db) =>
{
    var results = await db.Products
        .Where(p => string.IsNullOrEmpty(q) || p.Name.Contains(q))
        .Take(20)
        .ToListAsync();

    // Return HTML fragment, not JSON
    var html = string.Join("", results.Select(p =>
        $"<tr><td>{p.Name}</td><td>{p.Price:C}</td></tr>"));

    return Results.Content(html, "text/html");
});

app.MapDelete("/products/{id}", async (int id, AppDbContext db) =>
{
    var product = await db.Products.FindAsync(id);
    if (product is null) return Results.Content("", "text/html");
    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return Results.Content("", "text/html"); // empty = delete row
});

5.2. Razor Pages Partial Rendering

// Pages/Products/Index.cshtml.cs
public class IndexModel : PageModel
{
    public async Task<IActionResult> OnGetSearchAsync(string q)
    {
        // Detect htmx request
        if (Request.Headers.ContainsKey("HX-Request"))
        {
            Products = await _db.Products
                .Where(p => p.Name.Contains(q))
                .ToListAsync();
            return Partial("_ProductList", Products);
        }
        return Page(); // Full page for non-htmx
    }
}
<!-- _ProductList.cshtml — Razor Partial -->
@model List<Product>
@foreach (var p in Model)
{
    <tr>
        <td>@p.Name</td>
        <td>@p.Price.ToString("C")</td>
        <td>
            <button hx-delete="/products/@p.Id"
                    hx-target="closest tr"
                    hx-swap="outerHTML"
                    hx-confirm="Delete this product?">
                Delete
            </button>
        </td>
    </tr>
}

5.3. NuGet Package Support

The Htmx.TagHelpers package provides Tag Helpers for Razor, making it easier to write htmx attributes in .cshtml files. The Htmx package provides extension methods for detecting htmx requests and setting response headers (HX-Trigger, HX-Redirect, HX-Retarget).

6. When to Use htmx (and When Not To)

Great Fit ✅ Not Ideal ❌
CRUD applications, admin panels Offline-first apps (PWA)
Dashboards, reporting, data tables Drawing tools, game UIs, editors
E-commerce product listings, carts Real-time collaboration (Google Docs)
Content-heavy sites, blogs, CMS Mobile apps (React Native/Flutter)
Internal tools, back-office systems Complex drag-and-drop interfaces
Landing pages with form interactions Apps with heavy client-side processing

The htmx + Alpine.js Combo

When you need a bit of client-side logic (toggle menus, dropdowns, modals) but not enough to justify React — Alpine.js (~17KB) paired with htmx is the perfect combo. htmx handles server interactions, Alpine.js handles purely client-side UI state.

7. Real-World Performance

A notable 2026 case study: an enterprise dashboard migrating from React to htmx achieved a 70% reduction in client-side code and a 40% improvement in Time-to-Interactive. Key reasons:

-70% Client-side code after migration
-40% Time-to-Interactive improvement
0 Hydration cost — no JS framework to hydrate
  • Zero hydration cost: SPA frameworks must download JS → parse → hydrate DOM before becoming interactive. htmx: load 14KB → ready immediately.
  • HTML over the wire is smaller than JSON: With server-side templates (Razor, Jinja, Go templates), HTML fragments are often smaller than JSON + client-side template logic.
  • No client-side routing overhead: The browser handles navigation natively, htmx just swaps content.
  • CDN-cacheable fragments: HTML fragments can be cached at the CDN layer — JSON API responses are typically harder to cache.

8. Best Practices

8.1. Server Should Return Fragments, Not Full Pages

Detect the HX-Request: true header to identify htmx requests. If present → return partial HTML. If not → return the full page (progressive enhancement).

8.2. Use hx-boost for Progressive Enhancement

<body hx-boost="true">
  <!-- All links and forms are automatically AJAX-ified -->
  <!-- Fallback: works normally if JS is disabled -->
  <a href="/about">About</a>
</body>

hx-boost turns all links and forms into AJAX requests automatically — pages load faster because only the <body> is swapped instead of reloading the entire page. If JavaScript is disabled, everything still works through traditional navigation.

8.3. Use HX-Trigger Response Header for Events

// Server sends event after successful processing
Response.Headers.Append("HX-Trigger", "showToast");

// Client listens for the event
<div hx-trigger="showToast from:body"
     hx-get="/toast/success"
     hx-swap="innerHTML"></div>

8.4. Security Considerations

  • Always validate and sanitize input server-side — htmx doesn't change the attack surface compared to traditional forms.
  • Use CSRF tokens for all mutations (POST/PUT/DELETE) — htmx sends cookies automatically but you still need CSRF protection.
  • Be careful with hx-target pointing to sensitive elements — ensure the server only returns safe HTML.

9. htmx Development Timeline

2013
intercooler.js — the predecessor to htmx, created by Carson Gross.
2020
htmx 1.0 launched — a rewrite of intercooler.js, dropping jQuery dependency, smaller footprint.
2024
htmx 2.0 — dropped IE support, cleaned up API, added head merging and response validation.
2026 Q1
htmx 4.0 Beta — migrated from XHR to Fetch API, Idiomorph as default, <hx-partial>, View Transitions. Skipped version 3 because "we promised there would be no htmx 3.0".
2026 Q3 (expected)
htmx 4.0 Stable — official release expected in summer 2026.

Conclusion

htmx is not a "React killer" — it's a sensible alternative for problems where SPA frameworks are overkill. With its hypermedia-driven philosophy, ultra-lightweight footprint, and significant improvements in version 4.0, htmx proves that you don't need a 300KB JavaScript framework to build dynamic, smooth web applications.

If you're building CRUD apps, internal tools, or content-heavy sites — htmx paired with ASP.NET Core or any server-side backend is a choice worth serious consideration.

References