HTMX and the Server-Side Renaissance: Building Modern UIs Without JavaScript Frameworks
For a decade, the frontend world told us the same story: you need React, or Vue, or Angular, or Svelte. You need a build step, a bundler, a node_modules folder, a state management library, and an API layer that serializes everything to JSON. HTMX challenges that entire assumption. It lets you build modern, interactive web applications by returning HTML from the server — and in 2026, it has moved from curiosity to a legitimate choice for production applications.
What Is HTMX
HTMX is a small (14KB) JavaScript library that extends HTML with attributes for making HTTP requests and updating the DOM. Instead of building a JavaScript application that calls an API and renders JSON into HTML, you build a server application that returns HTML fragments directly.
<!-- Traditional SPA approach -->
<button onclick="fetchUsers().then(renderTable)">Load Users</button>
<!-- HTMX approach -->
<button hx-get="/api/users" hx-target="#user-table" hx-swap="innerHTML">
Load Users
</button>
The HTMX version sends a GET request to /api/users, takes the HTML response, and swaps it into the #user-table element. No JavaScript written. No JSON parsing. No virtual DOM diffing.
The Hypermedia Architecture
HTMX is built on the hypermedia approach — the idea that the server should drive application state through HTML, not JSON. This is actually how the web originally worked. HTMX just modernizes it with AJAX capabilities.
The core HTMX attributes:
| Attribute | Purpose | Example |
|---|---|---|
hx-get | Issue GET request | hx-get="/users" |
hx-post | Issue POST request | hx-post="/users" |
hx-put | Issue PUT request | hx-put="/users/1" |
hx-delete | Issue DELETE request | hx-delete="/users/1" |
hx-target | Where to put the response | hx-target="#results" |
hx-swap | How to swap the content | hx-swap="outerHTML" |
hx-trigger | What triggers the request | hx-trigger="click" |
hx-indicator | Loading indicator | hx-indicator="#spinner" |
hx-push-url | Update browser URL | hx-push-url="true" |
Building a Complete CRUD Application
Here is a full task management app using HTMX with a Spring Boot backend:
@Controller
@RequestMapping("/tasks")
public class TaskController {
private final TaskService taskService;
@GetMapping
public String listTasks(Model model) {
model.addAttribute("tasks", taskService.findAll());
return "tasks/list"; // Full page
}
@GetMapping("/table")
public String taskTable(Model model) {
model.addAttribute("tasks", taskService.findAll());
return "tasks/fragments :: task-table"; // HTML fragment
}
@PostMapping
public String createTask(@ModelAttribute TaskForm form, Model model) {
taskService.create(form);
model.addAttribute("tasks", taskService.findAll());
return "tasks/fragments :: task-table"; // Return updated table
}
@PutMapping("/{id}/toggle")
public String toggleTask(@PathVariable Long id, Model model) {
Task task = taskService.toggleComplete(id);
model.addAttribute("task", task);
return "tasks/fragments :: task-row"; // Return just the updated row
}
@DeleteMapping("/{id}")
public String deleteTask(@PathVariable Long id, Model model) {
taskService.delete(id);
model.addAttribute("tasks", taskService.findAll());
return "tasks/fragments :: task-table";
}
@GetMapping("/search")
public String searchTasks(@RequestParam String q, Model model) {
model.addAttribute("tasks", taskService.search(q));
return "tasks/fragments :: task-table";
}
}
<!-- tasks/list.html (Thymeleaf) -->
<!DOCTYPE html>
<html>
<head>
<title>Task Manager</title>
<script src="https://unpkg.com/htmx.org@2.0"></script>
-
</head>
<body>
<div class="container">
<h1>Task Manager</h1>
<!-- Search with live filtering -->
<input type="search"
name="q"
placeholder="Search tasks..."
hx-get="/tasks/search"
hx-target="#task-table"
hx-trigger="input changed delay:300ms"
hx-indicator="#search-spinner">
<span id="search-spinner" class="htmx-indicator">Searching...
<!-- Add new task form -->
<form hx-post="/tasks"
hx-target="#task-table"
hx-swap="innerHTML"
hx-on::after-request="this.reset()">
<input type="text" name="title" placeholder="New task..." required>
<button type="submit">Add Task</button>
</form>
<!-- Task table (updated via HTMX) -->
<div id="task-table">
<div th:fragment="task-table">
<div th:each="task : ${tasks}"
th:fragment="task-row"
th:id="'task-' + ${task.id}"
class="task-row">
<input type="checkbox"
th:checked="${task.completed}"
hx-put th:hx-put="'/tasks/' + ${task.id} + '/toggle'"
hx-target th:hx-target="'#task-' + ${task.id}"
hx-swap="outerHTML">
<span th:text="${task.title}"
th:classappend="${task.completed ? 'completed' : ''}">
<button hx-delete th:hx-delete="'/tasks/' + ${task.id}"
hx-target="#task-table"
hx-confirm="Delete this task?">
Delete
</button>
No tasks found.
</body>
</html>
This gives you:
Inline task creation without page reload
Toggle completion by clicking the checkbox
Delete with confirmation dialog
Loading indicators during requests
Zero JavaScript written. The entire interactivity is declared in HTML attributes.
HTMX Patterns for Common UI Interactions
Infinite Scroll:
<div id="feed">
<div class="post">Post 1...
<div class="post">Post 2...
<!-- Trigger loads next page when scrolled into view -->
<div hx-get="/feed?page=2"
hx-target="#feed"
hx-swap="beforeend"
hx-trigger="revealed">
<span class="htmx-indicator">Loading more...
Active Search with Debounce:
<input type="search"
hx-get="/search"
hx-target="#results"
hx-trigger="input changed delay:500ms, search"
hx-indicator="#spinner"
name="q">
Modal Dialog:
<button hx-get="/users/1/edit"
hx-target="#modal-container"
hx-swap="innerHTML">
Edit User
</button>
<div id="modal-container">
<!-- Server returns the modal HTML -->
<!-- GET /users/1/edit response: -->
<div class="modal-overlay" _="on click remove me">
<div class="modal" _="on click halt">
<form hx-put="/users/1"
hx-target="#user-1"
hx-swap="outerHTML"
hx-on::after-request="document.querySelector('.modal-overlay').remove()">
<input name="name" value="John Doe">
<input name="email" value="john@example.com">
<button type="submit">Save</button>
<button type="button" _="on click remove closest .modal-overlay">Cancel</button>
</form>
Polling for Live Updates:
<div hx-get="/notifications/count"
hx-trigger="every 30s"
hx-swap="innerHTML">
<span class="badge">3
HTMX vs React/Vue/Svelte: When to Use What
| Aspect | HTMX | React/Vue/Svelte |
|---|---|---|
| Bundle size | 14KB (HTMX) | 40-150KB (framework + runtime) |
| Build step | None | Webpack/Vite/Rollup required |
| State management | Server-side (sessions, DB) | Client-side (Redux, Zustand, etc.) |
| SEO | Perfect (server-rendered HTML) | Requires SSR/SSG setup |
| Offline support | Limited | Good (with service workers) |
| Complex UI state | Challenging | Native strength |
| Team skills needed | Backend developers | Frontend specialists |
| Time to production | Fast | Moderate |
| Real-time collaboration | Possible but limited | Native strength |
When HTMX Shines
CRUD applications — Admin panels, dashboards, content management systems
Server-rendered websites — Blogs, documentation, marketing sites with interactive elements
Internal tools — Where development speed matters more than UI polish
Small teams — Backend developers who want interactivity without learning React
Progressive enhancement — Adding dynamic behavior to existing server-rendered pages
When to Stick with SPAs
Rich interactive UIs — Real-time collaboration (Google Docs), complex drag-and-drop, canvas-based applications
Offline-first applications — PWAs that must work without a network connection
Complex client-side state — Shopping carts with optimistic updates, form wizards with branching logic
Native mobile targets — React Native / Flutter for cross-platform mobile
The Performance Story
HTMX applications are often faster than SPAs for initial load:
| Metric | HTMX App | React SPA |
|---|---|---|
| Initial HTML | 15KB | 5KB (shell) |
| JavaScript | 14KB (HTMX) | 180KB (React + app) |
| First Contentful Paint | 0.8s | 1.8s |
| Time to Interactive | 1.0s | 2.5s |
| Subsequent navigation | 50-100ms (HTML fragment) | 10-50ms (JSON + render) |
HTMX wins on initial load because there is almost no JavaScript to download and parse. SPAs can win on subsequent navigations because JSON is smaller than HTML fragments. In practice, the difference is negligible for most applications.
The Philosophy Shift
HTMX represents a philosophical shift: the browser is a hypermedia client, not an application platform. Instead of building two applications — a JavaScript frontend and a JSON API — you build one application that returns HTML.
This is not a step backward. It is a recognition that for many applications, the complexity of a full SPA is unnecessary overhead. You do not need a virtual DOM, a state management library, and a build pipeline to add a delete button that calls the server and removes a row from a table.
HTMX gives backend developers superpowers. If you spend your days writing Spring Boot, Django, Rails, or Express, and you want interactive UIs without becoming a frontend engineer, HTMX deserves a serious look. Build a prototype, compare the experience, and let the code speak for itself.