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 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.

Web Development

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:

AttributePurposeExample
hx-getIssue GET requesthx-get="/users"
hx-postIssue POST requesthx-post="/users"
hx-putIssue PUT requesthx-put="/users/1"
hx-deleteIssue DELETE requesthx-delete="/users/1"
hx-targetWhere to put the responsehx-target="#results"
hx-swapHow to swap the contenthx-swap="outerHTML"
hx-triggerWhat triggers the requesthx-trigger="click"
hx-indicatorLoading indicatorhx-indicator="#spinner"
hx-push-urlUpdate browser URLhx-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:

  • Live search with debouncing (300ms delay)
    • 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

    AspectHTMXReact/Vue/Svelte
    Bundle size14KB (HTMX)40-150KB (framework + runtime)
    Build stepNoneWebpack/Vite/Rollup required
    State managementServer-side (sessions, DB)Client-side (Redux, Zustand, etc.)
    SEOPerfect (server-rendered HTML)Requires SSR/SSG setup
    Offline supportLimitedGood (with service workers)
    Complex UI stateChallengingNative strength
    Team skills neededBackend developersFrontend specialists
    Time to productionFastModerate
    Real-time collaborationPossible but limitedNative 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:

    MetricHTMX AppReact SPA
    Initial HTML15KB5KB (shell)
    JavaScript14KB (HTMX)180KB (React + app)
    First Contentful Paint0.8s1.8s
    Time to Interactive1.0s2.5s
    Subsequent navigation50-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.

  • Scroll to Top