Przejdź do treści
Frontend

HTMX - Interactive Web Apps Without JavaScript. A Guide

Published on:
·7 min read·Author: MDS Software Solutions Group

HTMX Interactive Web

frontend

HTMX - Interactivity Without JavaScript

In an era dominated by complex JavaScript frameworks like React, Vue, and Angular, a library has emerged that proposes a radically different approach to building interactive web applications. HTMX allows you to create dynamic user interfaces directly from HTML, without writing a single line of JavaScript. Sounds like a step backward? Quite the opposite - it is an evolution of the hypermedia approach that became the foundation of the modern web.

In this article, we will explore the philosophy behind HTMX, discuss its key attributes and mechanisms, demonstrate practical implementation patterns, and compare this approach with popular SPA frameworks.

The HTMX Philosophy - HTML over the Wire#

HTMX is built on the concept of HTML over the Wire (HOTW), which posits that the server should return ready-made HTML fragments rather than raw JSON data. This is a return to the classic hypermedia model, where HTML serves as a fully-fledged medium of communication between client and server.

The traditional SPA approach works like this:

  1. The browser sends an AJAX request
  2. The server returns JSON data
  3. Client-side JavaScript parses the JSON
  4. JavaScript generates HTML and updates the DOM

HTMX simplifies this flow:

  1. The browser sends an AJAX request (via HTMX attributes)
  2. The server returns a ready-made HTML fragment
  3. HTMX automatically inserts the HTML into the appropriate place in the DOM

This approach eliminates the need to maintain presentation logic on the client side, significantly reducing the amount of JavaScript code required.

Installing HTMX#

Getting started with HTMX is remarkably simple. Just add a single script tag:

<!-- From CDN -->
<script src="https://unpkg.com/htmx.org@2.0.4"></script>

<!-- Or download locally -->
<script src="/js/htmx.min.js"></script>

HTMX weighs only about 14 KB after gzip compression - a fraction of the size of a typical JavaScript framework.

Core Attributes - hx-get, hx-post, hx-put, hx-delete#

HTMX extends HTML with a set of attributes that allow any HTML element to send HTTP requests. In standard HTML, only forms and links can initiate requests - HTMX removes this limitation.

hx-get - Fetching Data#

<!-- Clicking the button sends a GET request and loads the result into #result -->
<button hx-get="/api/articles" hx-target="#result">
  Load Articles
</button>

<div id="result">
  <!-- Articles will appear here -->
</div>

hx-post - Sending Data#

<!-- Form submitted via HTMX instead of traditional submit -->
<form hx-post="/api/comments" hx-target="#comments-list" hx-swap="beforeend">
  <input type="text" name="author" placeholder="Your name" required />
  <textarea name="content" placeholder="Comment text" required></textarea>
  <button type="submit">Add Comment</button>
</form>

<div id="comments-list">
  <!-- New comments will be appended here -->
</div>

hx-put and hx-delete - Updating and Deleting#

<!-- Editing an article -->
<form hx-put="/api/articles/42" hx-target="#article-42">
  <input type="text" name="title" value="Existing title" />
  <textarea name="content">Existing content</textarea>
  <button type="submit">Save Changes</button>
</form>

<!-- Deleting with confirmation -->
<button
  hx-delete="/api/articles/42"
  hx-target="#article-42"
  hx-swap="outerHTML"
  hx-confirm="Are you sure you want to delete this article?"
>
  Delete Article
</button>

Each of these attributes can transform any HTML element - a button, link, div, or even a span - into an interactive element that sends an HTTP request to the server.

hx-target and hx-swap - Precise DOM Management#

Two key HTMX attributes control where and how the server response is inserted.

hx-target#

The hx-target attribute specifies which DOM element should be updated with the server response. It accepts CSS selectors:

<!-- Update a specific element by ID -->
<button hx-get="/notifications" hx-target="#notification-panel">
  Check Notifications
</button>

<!-- Update the nearest parent element -->
<button hx-get="/user/profile" hx-target="closest .card">
  Refresh Profile
</button>

<!-- Update an element using the find selector -->
<div class="search-container">
  <input
    type="search"
    hx-get="/search"
    hx-target="find .results"
    name="q"
  />
  <div class="results"></div>
</div>

hx-swap#

The hx-swap attribute controls how the content is inserted. Available strategies:

<!-- innerHTML (default) - replace the element's content -->
<div hx-get="/content" hx-swap="innerHTML">Will be replaced</div>

<!-- outerHTML - replace the entire element -->
<div hx-get="/content" hx-swap="outerHTML">Entire div will be replaced</div>

<!-- beforeend - append at the end of the element -->
<ul hx-get="/more-items" hx-swap="beforeend">
  <li>Existing item</li>
  <!-- New items will appear here -->
</ul>

<!-- afterbegin - prepend at the start of the element -->
<ul hx-get="/new-items" hx-swap="afterbegin">
  <!-- New items will appear here -->
  <li>Existing item</li>
</ul>

<!-- beforebegin - insert before the element -->
<div hx-get="/content" hx-swap="beforebegin">
  New content will appear before this div
</div>

<!-- afterend - insert after the element -->
<div hx-get="/content" hx-swap="afterend">
  New content will appear after this div
</div>

<!-- delete - remove the target element -->
<button hx-delete="/items/1" hx-target="#item-1" hx-swap="delete">
  Delete
</button>

<!-- none - do not modify the DOM -->
<button hx-post="/api/track-click" hx-swap="none">
  Track Click
</button>

Additionally, hx-swap supports modifiers:

<!-- Swap animation with delay -->
<div hx-get="/content" hx-swap="innerHTML swap:300ms">
  Smooth content swap
</div>

<!-- Scroll to element after swap -->
<div hx-get="/content" hx-swap="innerHTML scroll:top">
  Scroll to top after swap
</div>

<!-- Settle animation -->
<div hx-get="/content" hx-swap="innerHTML settle:500ms">
  Content with animation
</div>

hx-trigger - Event Control#

The hx-trigger attribute defines which event initiates the HTTP request. By default, HTMX responds to click for buttons and links, change for form inputs, and submit for forms.

<!-- Custom triggers -->
<div hx-get="/news" hx-trigger="every 30s">
  Auto-refresh every 30 seconds
</div>

<!-- Trigger on mouse hover -->
<div hx-get="/preview" hx-trigger="mouseenter once">
  Hover to load preview (only once)
</div>

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

<!-- Multiple triggers -->
<form
  hx-post="/validate"
  hx-trigger="change, keyup delay:500ms changed"
  hx-target="#validation-result"
>
  <input type="email" name="email" />
</form>

<!-- Trigger on element load -->
<div hx-get="/stats" hx-trigger="load">
  Loading statistics...
</div>

<!-- Trigger on element visibility (Intersection Observer) -->
<div hx-get="/lazy-content" hx-trigger="revealed">
  Will load when the element becomes visible
</div>

AJAX Without JavaScript - Practical Patterns#

HTMX enables common interaction patterns that traditionally required writing JavaScript code. Here are the most popular ones.

Real-time search, similar to Google Suggest, without a single line of JS:

<div class="search-box">
  <input
    type="search"
    name="q"
    hx-get="/search"
    hx-trigger="keyup changed delay:300ms"
    hx-target="#search-results"
    hx-indicator=".search-spinner"
    placeholder="Type your query..."
  />
  <span class="search-spinner htmx-indicator">
    Searching...
  </span>
</div>

<div id="search-results">
  <!-- Search results will appear here -->
</div>

Server-side implementation (Python with Flask):

@app.route('/search')
def search():
    query = request.args.get('q', '')
    if len(query) < 2:
        return ''
    results = Article.query.filter(
        Article.title.ilike(f'%{query}%')
    ).limit(10).all()
    return render_template('partials/search_results.html', results=results)

Infinite Scroll#

<div id="articles-list">
  <div class="article-card">
    <h3>Article 1</h3>
    <p>Article content...</p>
  </div>
  <div class="article-card">
    <h3>Article 2</h3>
    <p>Article content...</p>
  </div>
  <!-- Sentinel element - loads more on scroll -->
  <div
    hx-get="/articles?page=2"
    hx-trigger="revealed"
    hx-swap="outerHTML"
    hx-indicator="#loading-spinner"
  >
    <div id="loading-spinner" class="htmx-indicator">
      Loading more articles...
    </div>
  </div>
</div>

The server returns additional articles along with a new sentinel:

<!-- Server response for /articles?page=2 -->
<div class="article-card">
  <h3>Article 3</h3>
  <p>Article content...</p>
</div>
<div class="article-card">
  <h3>Article 4</h3>
  <p>Article content...</p>
</div>
<!-- New sentinel for the next page -->
<div
  hx-get="/articles?page=3"
  hx-trigger="revealed"
  hx-swap="outerHTML"
>
  Loading...
</div>

Real-Time Form Validation#

<form hx-post="/register" hx-target="#form-messages">
  <div class="form-group">
    <label for="email">Email</label>
    <input
      type="email"
      id="email"
      name="email"
      hx-post="/validate/email"
      hx-trigger="change, keyup delay:500ms changed"
      hx-target="next .validation-message"
    />
    <span class="validation-message"></span>
  </div>

  <div class="form-group">
    <label for="username">Username</label>
    <input
      type="text"
      id="username"
      name="username"
      hx-post="/validate/username"
      hx-trigger="change, keyup delay:500ms changed"
      hx-target="next .validation-message"
    />
    <span class="validation-message"></span>
  </div>

  <div class="form-group">
    <label for="password">Password</label>
    <input type="password" id="password" name="password" />
  </div>

  <button type="submit">Register</button>
  <div id="form-messages"></div>
</form>

Server-side validation response:

@app.route('/validate/email', methods=['POST'])
def validate_email():
    email = request.form.get('email', '')
    if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email):
        return '<span class="validation-message error">Invalid email format</span>'
    if User.query.filter_by(email=email).first():
        return '<span class="validation-message error">This email is already taken</span>'
    return '<span class="validation-message success">Email available</span>'

Inline Editing (Click to Edit)#

<!-- Display mode -->
<div id="user-name" hx-get="/user/1/edit" hx-trigger="click" hx-swap="outerHTML">
  <span class="editable">John Smith</span>
  <small>(click to edit)</small>
</div>

The server returns the editing form:

<!-- Response from /user/1/edit -->
<form id="user-name" hx-put="/user/1" hx-target="this" hx-swap="outerHTML">
  <input type="text" name="name" value="John Smith" autofocus />
  <button type="submit">Save</button>
  <button hx-get="/user/1" hx-target="#user-name" hx-swap="outerHTML">Cancel</button>
</form>

HTMX is backend-agnostic - it works perfectly with any server that can return HTML. Here are integration examples with the most popular frameworks.

Django#

Django and HTMX are a natural fit thanks to Django's template system:

# views.py
from django.shortcuts import render
from django.http import HttpResponse
from .models import Task

def task_list(request):
    tasks = Task.objects.all()
    if request.headers.get('HX-Request'):
        return render(request, 'partials/task_list.html', {'tasks': tasks})
    return render(request, 'tasks/index.html', {'tasks': tasks})

def create_task(request):
    if request.method == 'POST':
        task = Task.objects.create(
            title=request.POST['title'],
            description=request.POST.get('description', '')
        )
        return render(request, 'partials/task_item.html', {'task': task})

def delete_task(request, task_id):
    Task.objects.filter(id=task_id).delete()
    return HttpResponse('')
<!-- templates/tasks/index.html -->
{% extends "base.html" %}

{% block content %}
<h1>Task List</h1>

<form hx-post="{% url 'create_task' %}" hx-target="#task-list" hx-swap="beforeend">
  {% csrf_token %}
  <input type="text" name="title" placeholder="New task..." required />
  <button type="submit">Add</button>
</form>

<div id="task-list">
  {% for task in tasks %}
    {% include "partials/task_item.html" %}
  {% endfor %}
</div>
{% endblock %}

There is also a dedicated django-htmx package that simplifies detecting HTMX requests and provides middleware with useful helpers.

Laravel#

// routes/web.php
Route::get('/tasks', [TaskController::class, 'index']);
Route::post('/tasks', [TaskController::class, 'store']);
Route::delete('/tasks/{task}', [TaskController::class, 'destroy']);

// TaskController.php
class TaskController extends Controller
{
    public function index(Request $request)
    {
        $tasks = Task::latest()->get();

        if ($request->header('HX-Request')) {
            return view('partials.task-list', compact('tasks'));
        }

        return view('tasks.index', compact('tasks'));
    }

    public function store(Request $request)
    {
        $task = Task::create($request->validate([
            'title' => 'required|string|max:255',
        ]));

        return view('partials.task-item', compact('task'));
    }

    public function destroy(Task $task)
    {
        $task->delete();
        return response('', 200);
    }
}
<!-- resources/views/partials/task-item.blade.php -->
<div class="task-item" id="task-{{ $task->id }}">
  <span>{{ $task->title }}</span>
  <button
    hx-delete="/tasks/{{ $task->id }}"
    hx-target="#task-{{ $task->id }}"
    hx-swap="outerHTML"
    hx-confirm="Delete this task?"
  >
    Delete
  </button>
</div>

Ruby on Rails#

Rails with its built-in partial system integrates perfectly with HTMX:

# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def index
    @tasks = Task.all.order(created_at: :desc)
  end

  def create
    @task = Task.create!(task_params)
    render partial: 'tasks/task', locals: { task: @task }
  end

  def destroy
    Task.find(params[:id]).destroy
    head :ok
  end

  private

  def task_params
    params.require(:task).permit(:title, :description)
  end
end

WebSocket and SSE - Real-Time Communication#

HTMX supports real-time communication through WebSocket and Server-Sent Events (SSE) extensions.

Server-Sent Events (SSE)#

SSE is a one-way server-to-client communication channel, ideal for notifications and live updates:

<div hx-ext="sse" sse-connect="/events/notifications">
  <div id="notifications" sse-swap="notification" hx-swap="beforeend">
    <!-- New notifications will be added automatically -->
  </div>

  <div id="online-count" sse-swap="online-count">
    <!-- Online user count updated in real time -->
  </div>
</div>

Python SSE server example:

from flask import Response
import json

@app.route('/events/notifications')
def notification_stream():
    def generate():
        while True:
            # Fetch new notifications
            notifications = get_pending_notifications()
            for notification in notifications:
                data = render_template('partials/notification.html',
                                       notification=notification)
                yield f"event: notification\ndata: {data}\n\n"
            time.sleep(1)
    return Response(generate(), mimetype='text/event-stream')

WebSocket#

<div hx-ext="ws" ws-connect="/ws/chat">
  <div id="chat-messages">
    <!-- Chat messages appear automatically -->
  </div>

  <form ws-send>
    <input type="text" name="message" placeholder="Type a message..." />
    <button type="submit">Send</button>
  </form>
</div>

HTMX vs React/Vue - When to Choose What?#

Comparing HTMX with SPA frameworks is not a matter of "better vs worse," but rather "the right tool for the right job."

When HTMX Is the Better Choice#

  • CRUD applications - admin panels, content management systems, dashboards
  • Pages with dynamic elements - forms, search bars, table filtering
  • Small team projects - one fullstack developer instead of separate frontend and backend teams
  • Existing server-rendered apps - adding interactivity to Django, Rails, or Laravel without rewriting as an SPA
  • Prototyping - rapid MVP development with minimal overhead
  • SEO-first applications - blogs, corporate websites, e-commerce with SSR

When React/Vue Is the Better Choice#

  • Advanced user interfaces - graphic editors, interactive maps, real-time collaborative apps
  • Offline-first applications - Progressive Web Apps with Service Workers
  • Complex client-side state - multi-step forms, wizards, drag-and-drop
  • Ecosystem and libraries - vast number of ready-made components
  • Large frontend teams - standardized component-based architecture

Technical Comparison#

| Feature | HTMX | React/Vue | |---------|------|-----------| | Size | ~14 KB | ~40-100 KB+ | | Language | HTML + attributes | JavaScript/TypeScript | | Application state | Server | Client | | Rendering | Server | Client (or SSR) | | Learning curve | Low | Medium-High | | Build tools | None | Webpack/Vite | | SEO | Native | Requires SSR/SSG | | Offline | Limited | Full support | | Testing | Integration tests | Unit + integration |

The Hypermedia Approach - Back to the Roots#

HTMX represents a broader industry trend - a return to the hypermedia approach. Instead of treating HTML as a "dumb" presentation format and moving all logic into JavaScript, the hypermedia approach recognizes HTML as a fully-fledged communication medium in web applications.

Key principles of the hypermedia approach:

  • Server manages state - no state duplication between client and server
  • HTML as the exchange format - the server returns ready-made interface fragments
  • Progressive enhancement - the application works even without JavaScript (in its basic form)
  • Architectural simplicity - no need for a separate API and frontend application
  • Less code - typically 67% less code compared to the SPA approach

HATEOAS in Practice#

HTMX naturally implements the HATEOAS principle (Hypermedia as the Engine of Application State), where the server controls available actions through returned links and forms:

<!-- Server returns different actions depending on state -->
<div class="article" id="article-42">
  <h2>Article Title</h2>
  <p>Article content...</p>

  <!-- Actions available to the author -->
  <div class="actions">
    <button hx-get="/articles/42/edit" hx-target="#article-42">Edit</button>
    <button hx-post="/articles/42/publish" hx-target="#article-42">Publish</button>
    <button hx-delete="/articles/42" hx-target="#article-42" hx-swap="outerHTML">Delete</button>
  </div>
</div>

Advanced HTMX Techniques#

Loading Indicators#

<button hx-get="/slow-request" hx-indicator="#spinner">
  Load Data
  <span id="spinner" class="htmx-indicator">
    <img src="/spinner.gif" alt="Loading..." />
  </span>
</button>

<style>
  .htmx-indicator {
    display: none;
  }
  .htmx-request .htmx-indicator,
  .htmx-request.htmx-indicator {
    display: inline;
  }
</style>

Request and Response Headers#

HTMX automatically sends useful headers with each request:

  • HX-Request: true - identifies the request as an HTMX request
  • HX-Target - ID of the target element
  • HX-Trigger - ID of the element that triggered the request
  • HX-Current-URL - current page URL

The server can control HTMX behavior through response headers:

# Client-side redirect
response.headers['HX-Redirect'] = '/dashboard'

# Full page refresh
response.headers['HX-Refresh'] = 'true'

# Trigger a client-side event
response.headers['HX-Trigger'] = json.dumps({
    'showToast': {'message': 'Saved successfully!', 'type': 'success'}
})

HTMX Extensions#

HTMX offers an extension system that adds additional functionality:

<!-- class-tools extension -->
<div hx-ext="class-tools">
  <div classes="add highlight:1s, remove highlight:3s">
    Animated CSS class
  </div>
</div>

<!-- response-targets extension - error handling -->
<form
  hx-ext="response-targets"
  hx-post="/submit"
  hx-target="#success"
  hx-target-422="#errors"
  hx-target-500="#server-error"
>
  <!-- form fields -->
</form>

When Is HTMX the Right Choice?#

HTMX works best when:

  • Your application is server-rendered - Django, Laravel, Rails, Spring, ASP.NET
  • You need dynamic elements but not a full SPA - search bars, filters, forms
  • You want to reduce project complexity - one stack instead of two
  • Your team has strong backend skills - no need to learn a JS framework
  • Performance matters - smaller bundle, faster loading
  • You are building an MVP or prototype - fast iteration without framework overhead
  • You are modernizing an existing application - gradually adding interactivity

However, HTMX is not a silver bullet. For applications that require advanced client-side state management, offline capabilities, or complex animations, frameworks like React or Vue remain the better choice.

Conclusion#

HTMX is a library that challenges the dominant paradigm of building web applications. Instead of shifting ever more logic to the client side, it proposes a return to a model where the server is the center of the application and HTML is a fully-fledged communication medium. With simple HTML attributes, developers can build interactive interfaces without writing JavaScript, significantly reducing project complexity and accelerating development.

The growing popularity of HTMX - over 30,000 stars on GitHub and an active community - demonstrates that the hypermedia approach resonates with an increasing number of developers who are looking for simpler ways to build modern web applications.


Looking for Experts in Modern Web Technologies?#

MDS Software Solutions Group helps companies choose and implement the right technologies for building high-performance web applications. Whether you are considering HTMX for a simpler architecture or need an advanced SPA solution - our team of experienced developers will help you make the best decision.

We offer:

  • Designing and building web applications with modern technologies
  • Modernizing existing systems and adding interactivity
  • Technology consulting and architecture audits
  • Backend integrations with Django, Laravel, Rails, and other frameworks

Get in touch with us and let's discuss your project.

Author
MDS Software Solutions Group

Team of programming experts specializing in modern web technologies.

HTMX - Interactive Web Apps Without JavaScript. A Guide | MDS Software Solutions Group | MDS Software Solutions Group