htmx — Building Dynamic Web Apps Without JavaScript Frameworks
Posted on: 4/25/2026 5:15:45 PM
Table of contents
- 1. What is htmx?
- 2. Core Attributes — Everything Starts from HTML
- 3. Hypermedia vs SPA Architecture
- 4. htmx 4.0 — The Big Leap in 2026
- 5. Integrating htmx with ASP.NET Core
- 6. When to Use htmx (and When Not To)
- 7. Real-World Performance
- 8. Best Practices
- 9. htmx Development Timeline
- Conclusion
- References
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.
Practical Example: Live Search
<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:
- 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-targetpointing to sensitive elements — ensure the server only returns safe HTML.
9. htmx Development Timeline
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
Prometheus + Grafana — Building a Production Monitoring Stack
Hono — The Ultralight Web Framework for Edge Computing
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.