Pavan Rangani

HomeBlogHTMX and the Server-Side Renaissance: Building Modern UIs Without JavaScript Frameworks

HTMX and the Server-Side Renaissance: Building Modern UIs Without JavaScript Frameworks

By Pavan Rangani · February 7, 2026 · Web Development

HTMX and the Server-Side Renaissance: Building Modern UIs Without JavaScript Frameworks

HTMX and the Server-Side Renaissance: Building Modern UIs Without JavaScript Frameworks

For more than 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 as of 2026 it has moved from curiosity to a legitimate, boring-in-the-best-way choice for production applications.

What Is HTMX

HTMX is a small (roughly 14KB gzipped) 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, and the library wires those fragments into the page for you.

<!-- 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, and no virtual DOM diffing. Importantly, the button still works as plain HTML if scripting fails, which is the foundation of progressive enhancement.

Web Development

The Hypermedia Architecture

HTMX is built on the hypermedia approach: the idea that the server should drive application state through HTML, not through JSON exchanged with a client-side state machine. This is actually how the web originally worked, with links and forms as the engine of application state. HTMX simply modernizes that model with AJAX, partial updates, and event triggers so that you no longer have to reload the whole page for every interaction.

The core attributes are few enough to learn in an afternoon:

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 task management app using HTMX with a Spring Boot backend. Notice that the controller returns Thymeleaf fragments rather than serialized objects, so the server stays the single source of rendered truth:

@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";
    }
}

The template declares all of its interactivity in attributes. A search box that fires after the user pauses typing, an inline form that posts a new task, and a checkbox that toggles completion are each one element with a couple of hx-* attributes:

<!-- Search with live, debounced 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">

<!-- Add new task; reset the form after the request completes -->
<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>

That handful of declarations gives you live search with debouncing, inline creation without a page reload, toggle-by-click completion, delete with a confirmation dialog via hx-confirm, and loading indicators during requests, all with zero JavaScript written.

HTMX Patterns for Common UI Interactions

Most of the UI behaviors that drive teams to a full framework turn out to be one-liners. Infinite scroll uses the revealed trigger, which fires when an element scrolls into the viewport:

<div hx-get="/feed?page=2" hx-target="#feed" hx-swap="beforeend"
     hx-trigger="revealed">
    <span class="htmx-indicator">Loading more...</span>
</div>

Polling for live updates is the every trigger, which replaces the hand-rolled setInterval plus fetch dance you would otherwise write:

<div hx-get="/notifications/count" hx-trigger="every 30s" hx-swap="innerHTML">
    <span class="badge">3</span>
</div>

And a modal dialog is just a fragment swapped into a container; the server returns the modal markup, and a tiny sprinkle of hyperscript (HTMX’s optional companion) closes it on outside click. For richer client-side behavior, HTMX pairs naturally with Alpine.js, which handles purely local state like a dropdown’s open/closed flag without involving the server at all.

Handling Errors and the Edge Cases

The happy path is easy; the honest question is what happens when a request fails. By default HTMX only swaps content for 2xx responses, so a 500 from the server leaves the page untouched and silent, which is rarely what you want. The fix is to listen for the htmx:responseError event and show a banner, or to configure the server to return an error fragment with the right swap. Validation is the other common case: instead of returning a 422 that HTMX ignores, return the form fragment re-rendered with inline error messages and a 200 status, so the user sees exactly which field failed. You will also want to handle the back button correctly by pairing hx-push-url with server routes that can render a full page when hit directly, not just a fragment, otherwise a bookmarked or refreshed deep link returns a naked partial.

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 Strong (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

HTMX is at its best for server-centric applications where the database, not the browser, is the real home of application state:

  • CRUD applications — admin panels, dashboards, and content management systems

  • Server-rendered websites — blogs, documentation, and marketing sites with interactive touches

  • Internal tools — where development speed and maintainability matter more than pixel-perfect polish

  • Small teams — backend developers who want interactivity without standing up a separate frontend pipeline

  • Progressive enhancement — adding dynamic behavior to existing server-rendered pages incrementally

When to Stick with SPAs (the Honest Trade-offs)

HTMX is not a universal replacement, and pretending otherwise does new adopters a disservice. There is a genuine cost to the round-trip model: every interaction is a network request, so a flaky or high-latency connection feels worse than an SPA that has already loaded its logic and can respond optimistically. Reach for a full framework when you have:

  • Rich interactive UIs — real-time collaboration like Google Docs, complex drag-and-drop, or canvas and WebGL applications

  • Offline-first requirements — PWAs that must function with no network at all

  • Heavy client-side state — shopping carts with optimistic updates, or multi-step wizards with branching logic that would be chatty over the wire

  • Native mobile targets — where React Native or Flutter share code across platforms

There is also a scaling consideration on the server: because rendering happens server-side, a traffic spike that an SPA would absorb on users’ devices instead lands on your application servers, so caching fragments and sizing your fleet matter more. And while the per-feature code is smaller, very large HTMX apps can sprawl into many fragment endpoints, which needs the same naming discipline you would apply to any growing codebase.

The Performance Story

HTMX applications are often faster than SPAs for the initial load, mainly because there is almost no JavaScript to download, parse, and execute before the page becomes interactive:

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)

The figures above are representative of common benchmarks rather than any single measurement, and the pattern they show is consistent: HTMX wins decisively on first paint because the browser does not block on a large bundle, while SPAs can edge ahead on subsequent navigations because a JSON payload is often smaller than the equivalent rendered HTML fragment. For most line-of-business applications, the subsequent-navigation gap is small enough to be imperceptible.

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 nostalgia or a step backward; rather, it is a recognition that for a large class of applications, the full machinery of an SPA is overhead you pay without recouping the value. You do not need a virtual DOM, a state container, and a build pipeline just to add a delete button that calls the server and removes a row from a table.

For deeper reference material, the MDN Web Docs cover the underlying HTTP and DOM semantics, and the web.dev best practices are useful for the performance side. If you build with Spring Boot, the same backend skills that power a REST API translate directly, and you may find our companion notes on API rate limiting in Spring Boot handy once your fragment endpoints start taking real traffic.

In conclusion, the HTMX server-side renaissance gives backend developers superpowers for the kinds of applications they build most. If you spend your days writing Spring Boot, Django, Rails, or Express and you want interactive UIs without becoming a full-time frontend engineer, HTMX deserves a serious look. Build a prototype, weigh the round-trip trade-offs honestly against your latency and interactivity needs, and let the code speak for itself.

← Back to all articles