Skip to main content
Technology & EngineeringHtmx269 lines

Htmx Progressive Enhancement

Progressive enhancement strategies for building HTMX applications that work without JavaScript

Quick Summary23 lines
You are an expert in building progressively enhanced web applications with HTMX, ensuring core functionality works without JavaScript while delivering an enhanced experience when HTMX is available.

## Key Points

1. **Baseline:** Standard HTML links and forms. Full page loads on every interaction.
2. **Boosted:** `hx-boost="true"` converts links and forms to AJAX with zero markup changes.
3. **Targeted:** `hx-target` and `hx-swap` update specific page regions for a single-page-app feel.
4. **Real-time:** WebSockets or SSE add live updates on top of the targeted layer.
- **Without `HX-Request` header:** Return a full HTML page.
- **With `HX-Request` header:** Return an HTML fragment.
- **Design server endpoints to serve both modes.** Every URL should return a full page for normal requests and a fragment for HTMX requests. This is the foundation of progressive enhancement.

## Quick Example

```html
<form action="/contacts" method="post">
  <input name="name" required>
  <input name="email" type="email" required>
  <button type="submit">Save</button>
</form>
```
skilldb get htmx-skills/Htmx Progressive EnhancementFull skill: 269 lines
Paste into your CLAUDE.md or agent config

Progressive Enhancement Strategies — HTMX

You are an expert in building progressively enhanced web applications with HTMX, ensuring core functionality works without JavaScript while delivering an enhanced experience when HTMX is available.

Overview

Progressive enhancement is the practice of building a fully functional baseline experience using standard HTML, forms, and links, then layering on HTMX to enhance the interaction model. When JavaScript is unavailable (blocked, failed to load, or disabled), the application still works through normal page navigation and form submissions. HTMX is uniquely suited to this approach because its server-side architecture naturally produces HTML that works both ways.

Core Concepts

The Enhancement Spectrum

  1. Baseline: Standard HTML links and forms. Full page loads on every interaction.
  2. Boosted: hx-boost="true" converts links and forms to AJAX with zero markup changes.
  3. Targeted: hx-target and hx-swap update specific page regions for a single-page-app feel.
  4. Real-time: WebSockets or SSE add live updates on top of the targeted layer.

Each layer builds on the last. If any layer fails, the layers below still function.

hx-boost — The Simplest Enhancement

Adding hx-boost="true" to the <body> (or any container) converts all child <a> and <form> elements to use AJAX. Links perform a GET and swap the <body>, forms submit via AJAX. The URL bar updates, history works, and no other markup changes are required.

Without JavaScript, these are standard links and forms that work via full page loads.

Dual-Purpose Endpoints

The server must return appropriate content for both HTMX and non-HTMX requests:

  • Without HX-Request header: Return a full HTML page.
  • With HX-Request header: Return an HTML fragment.

This is the cornerstone of progressive enhancement with HTMX.

Implementation Patterns

Boosted Navigation

<body hx-boost="true">
  <nav>
    <a href="/dashboard">Dashboard</a>
    <a href="/contacts">Contacts</a>
    <a href="/settings">Settings</a>
  </nav>
  <main id="content">
    <!-- Page content here -->
  </main>
</body>

With HTMX: clicking a link fetches the page via AJAX and swaps the <body>. Without HTMX: clicking a link performs a standard browser navigation.

No markup changes needed. The links are valid, semantic, accessible HTML either way.

Progressive Form Enhancement

Start with a working HTML form:

<form action="/contacts" method="post">
  <input name="name" required>
  <input name="email" type="email" required>
  <button type="submit">Save</button>
</form>

Layer on HTMX attributes for an enhanced experience:

<form action="/contacts" method="post"
      hx-post="/contacts"
      hx-target="#contact-list"
      hx-swap="beforeend"
      hx-on::after-request="this.reset()">
  <input name="name" required>
  <input name="email" type="email" required>
  <button type="submit" hx-disabled-elt="this">Save</button>
</form>

With HTMX: the form submits via AJAX, appends the new contact to the list, and resets. Without HTMX: the form submits normally, the page reloads, and the user sees the updated list.

Server-Side Pattern for Dual Responses

# Django view
def create_contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST)
        if form.is_valid():
            contact = form.save()
            if request.headers.get("HX-Request"):
                # HTMX: return just the new contact row
                return render(request, "contacts/_row.html", {"contact": contact})
            else:
                # Standard: redirect (PRG pattern)
                return redirect("/contacts")
        else:
            if request.headers.get("HX-Request"):
                return render(request, "contacts/_form.html", {"form": form}, status=422)
            else:
                return render(request, "contacts/new.html", {"form": form})

    form = ContactForm()
    return render(request, "contacts/new.html", {"form": form})

Progressive Delete with Confirmation

Baseline (no JS):

<form action="/contacts/42/delete" method="post">
  <button type="submit"
          onclick="return confirm('Delete this contact?')">
    Delete
  </button>
</form>

Enhanced with HTMX:

<form action="/contacts/42/delete" method="post">
  <button type="submit"
          hx-delete="/contacts/42"
          hx-confirm="Delete this contact?"
          hx-target="closest tr"
          hx-swap="outerHTML swap:500ms">
    Delete
  </button>
</form>

When HTMX is present, it intercepts the button click, shows its own confirmation, and removes the table row with an animation. Without HTMX, the standard form submission and onclick confirmation still work.

Progressive Search

Baseline search form:

<form action="/contacts" method="get">
  <input type="search" name="q" placeholder="Search...">
  <button type="submit">Search</button>
</form>
<div id="results">
  <!-- server-rendered results -->
</div>

Enhanced:

<form action="/contacts" method="get">
  <input type="search" name="q" placeholder="Search..."
         hx-get="/contacts"
         hx-trigger="input changed delay:300ms, search"
         hx-target="#results"
         hx-push-url="true"
         hx-indicator="#spinner">
  <button type="submit">Search</button>
  <span id="spinner" class="htmx-indicator">Searching...</span>
</form>
<div id="results">
  <!-- server-rendered results -->
</div>

Without HTMX: the user types and presses Enter or clicks Search, which submits the form normally. With HTMX: results update as the user types, the URL updates, and the search button still works.

Pagination That Degrades Gracefully

<div id="user-list">
  {% for user in users %}
    <div class="user-card">{{ user.name }}</div>
  {% endfor %}
</div>

<!-- Works as a standard link without HTMX -->
{% if next_page %}
  <a href="/users?page={{ next_page }}"
     hx-get="/users?page={{ next_page }}"
     hx-target="#user-list"
     hx-swap="innerHTML"
     hx-push-url="true">
    Next Page
  </a>
{% endif %}

Noscript Fallback Messaging

<noscript>
  <style>
    .htmx-indicator { display: none !important; }
    .js-only { display: none !important; }
  </style>
</noscript>

This hides HTMX-specific UI elements (spinners, JS-only buttons) when JavaScript is disabled.

Feature Detection in Templates

<!-- Show inline delete for HTMX users, link for others -->
<div class="contact-actions">
  <a href="/contacts/42/delete"
     class="js-only"
     hx-delete="/contacts/42"
     hx-target="closest .contact-card"
     hx-swap="outerHTML"
     hx-confirm="Delete?">
    Delete
  </a>
  <noscript>
    <a href="/contacts/42/delete">Delete</a>
  </noscript>
</div>

Core Philosophy

Progressive enhancement with HTMX is not a fallback strategy; it is the primary development methodology. You build a fully functional application using standard HTML links and forms first, then enhance it with HTMX attributes. This order matters because it guarantees that the baseline experience always works, and every HTMX attribute is a genuine improvement rather than a dependency.

The hx-boost attribute embodies this philosophy perfectly. Adding a single attribute to the <body> element converts every link and form on the site from full-page navigation to AJAX-powered partial updates. No markup changes, no JavaScript, no build step. The result is a site that feels like a single-page application but degrades to standard multi-page navigation when JavaScript is unavailable. This is the lowest-effort, highest-impact enhancement possible.

Progressive enhancement also means that every URL in your application is a real, bookmarkable, shareable address that returns a full page. If a user saves a search URL, shares a product link, or refreshes a form, they get a complete, functioning page. This is the web working as designed, and HTMX preserves this contract while adding the smooth, partial-update experience that users expect from modern applications.

Anti-Patterns

  • Building HTMX-only endpoints that return fragments without a full-page fallback. If a URL only returns a fragment, direct access (bookmark, refresh, back button) shows broken or partial content. Every URL must return a full page for non-HTMX requests and a fragment for HTMX requests.

  • Replacing semantic HTML elements with HTMX-driven JavaScript-only patterns. Using <div hx-get> instead of <a href> for navigation or <span hx-post> instead of <form> for mutations breaks the baseline. Always use proper semantic elements as the foundation.

  • Assuming hx-boost handles all interaction patterns. Boost converts links and forms to AJAX but does not support file uploads (which need explicit hx-encoding), live search (which needs hx-trigger), or infinite scroll. Know when to move beyond boost to targeted HTMX attributes.

  • Not testing with JavaScript disabled. If you never disable JavaScript in the browser during development, you have no way to verify that the baseline experience works. Make no-JS testing a regular part of your development workflow.

  • Relying on JavaScript-only UI patterns without server-rendered alternatives. Modals, tabs, and accordions that exist only in JavaScript have no baseline. Provide server-rendered standalone pages for these interactions, then layer HTMX on top to inline them.

Best Practices

  • Start with hx-boost. It is the lowest-effort, highest-impact enhancement. Adding hx-boost="true" to the body instantly makes your multi-page application feel like a SPA, with no other changes.
  • Keep URLs meaningful. Use hx-push-url="true" so the browser URL always reflects the current state. This ensures bookmarking, sharing, and the back button work identically with or without HTMX.
  • Design server endpoints to serve both modes. Every URL should return a full page for normal requests and a fragment for HTMX requests. This is the foundation of progressive enhancement.
  • Use semantic HTML. Real links (<a href="...">), real forms (<form action="..." method="post">), and proper button types ensure the baseline works. HTMX attributes are additive, not replacements.
  • Test without JavaScript. Regularly disable JavaScript in the browser and verify that all critical paths work: navigation, form submission, search, pagination. Treat this as a first-class testing concern.
  • Use HTTP status codes correctly. A redirect after POST (303 See Other) works for standard forms. For HTMX, use HX-Redirect. The server can return the appropriate response based on the HX-Request header.

Common Pitfalls

  • Building HTMX-only endpoints. If an endpoint only returns a fragment and never a full page, it breaks when accessed directly (bookmark, refresh, back button without hx-push-url). Every URL the user can land on must return a full page for non-HTMX requests.
  • Using hx-boost with third-party scripts. Boosted navigation replaces the <body> but does not re-run <script> tags in the <head>. Third-party widgets that initialize on DOMContentLoaded will not reinitialize. Use htmx:afterSettle to reinitialize them.
  • Relying on JavaScript-only UI patterns. Modals, accordions, and tabs that only work with JavaScript have no baseline. Provide server-rendered fallback pages (e.g., the modal content as a standalone page) that HTMX enhances into inline interactions.
  • Forgetting the POST/Redirect/GET pattern. Without HTMX, a successful POST should redirect to avoid duplicate submissions on refresh. With HTMX, use HX-Redirect. The server must handle both paths.
  • Assuming hx-boost handles everything. Boosted forms do not support file uploads (they need hx-encoding="multipart/form-data" explicitly). Complex interactions (infinite scroll, live search) require targeted HTMX attributes beyond boost.
  • Not testing cached page loads. Browsers cache pages for back/forward navigation (bfcache). HTMX-enhanced pages may show stale content when navigated to via the back button. Use the htmx:historyRestore event to refresh stale content.

Install this skill directly: skilldb add htmx-skills

Get CLI access →