HTMX Loading Indicators

When HTMX sends a request, the server may take a moment to respond. During that wait, users need visual feedback. Without it, they click buttons repeatedly, unsure whether anything happened. HTMX provides a built-in loading indicator system that shows and hides feedback automatically — no JavaScript required.

The Problem: Silent Requests Feel Broken

  WITHOUT a loading indicator:

  User clicks [Submit]
       |
       | (2 seconds pass)
       | Page is silent — nothing moves
       |
  User thinks: "Did it work? Should I click again?"
       |
       | User clicks again
       v
  Double submission — data saved twice!
  WITH a loading indicator:

  User clicks [Submit]
       |
       | Spinner appears immediately
       | "Saving..." text visible
       |
  User thinks: "It's working. I'll wait."
       v
  Response arrives → spinner hides → success message shows

The htmx-indicator Class

HTMX watches for elements that have the class htmx-indicator. By default, these elements are hidden (opacity 0). The moment an HTMX request starts, HTMX adds the class htmx-request to the triggering element. Any htmx-indicator element near the trigger becomes visible. When the response arrives, the indicator hides again.

Basic Example: Button With Spinner

<button hx-post="/save" hx-target="#result">
  Save
  <span class="htmx-indicator">Saving...</span>
</button>

<div id="result"></div>

When the user clicks Save, the text "Saving..." appears next to the button label. When the server responds, "Saving..." disappears and the result div fills with the server's response.

Using hx-indicator to Point at Any Element

You can place the loading indicator anywhere on the page — not just inside the trigger element. Use the hx-indicator attribute and a CSS selector to point at it:

<button hx-get="/report" hx-target="#report-output" hx-indicator="#loading-bar">
  Generate Report
</button>

<div id="loading-bar" class="htmx-indicator">
  Generating your report, please wait...
</div>

<div id="report-output"></div>
  Diagram:

  [Generate Report] button  ──hx-indicator──> #loading-bar

  On click:
    Button sends request
    #loading-bar becomes visible

  On response:
    #report-output fills with HTML
    #loading-bar hides again

Adding a CSS Spinner

The htmx-indicator class alone only controls visibility. You style the indicator however you like. A simple CSS spinner looks like this:

<style>
  .spinner {
    display: inline-block;
    width: 20px;
    height: 20px;
    border: 3px solid #ccc;
    border-top-color: #333;
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
    opacity: 0;
    transition: opacity 0.2s;
  }
  .htmx-request .spinner,
  .htmx-request.spinner {
    opacity: 1;
  }
  @keyframes spin {
    to { transform: rotate(360deg); }
  }
</style>

<button hx-post="/process" hx-target="#output">
  Process
  <span class="spinner htmx-indicator"></span>
</button>

<div id="output"></div>

The spinner element stays invisible at opacity 0. When HTMX adds htmx-request to the button, the CSS rule .htmx-request .spinner sets opacity to 1 and the animation plays.

Disabling the Button During Request

You can also disable the trigger button during a request to prevent double submissions. HTMX does not do this automatically, but the htmx-request class gives you a CSS hook:

<style>
  button.htmx-request {
    opacity: 0.5;
    pointer-events: none;
    cursor: not-allowed;
  }
</style>

When the request is in flight, the button fades and becomes unclickable. When the response arrives, it returns to normal automatically — without a single line of JavaScript.

Full-Page Overlay Indicator

For long-running operations, a full-page overlay prevents all interaction until the request completes:

<style>
  #overlay {
    display: none;
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background: rgba(0,0,0,0.4);
    align-items: center;
    justify-content: center;
  }
  #overlay.htmx-request {
    display: flex;
  }
</style>

<div id="overlay" class="htmx-indicator">
  <p>Processing... Please wait.</p>
</div>

<button hx-post="/long-task" hx-target="#result" hx-indicator="#overlay">
  Start Task
</button>

<div id="result"></div>

Multiple Indicators for Multiple Requests

Each HTMX element tracks its own request independently. You can have several elements making simultaneous requests, each with its own indicator:

<div hx-get="/widget-a" hx-trigger="load" hx-target="this">
  <span class="htmx-indicator">Loading Widget A...</span>
</div>

<div hx-get="/widget-b" hx-trigger="load" hx-target="this">
  <span class="htmx-indicator">Loading Widget B...</span>
</div>

Each widget loads independently. Their indicators appear and disappear on their own schedules.

HTMX Request Lifecycle Classes

ClassAdded ToWhen
htmx-requestTriggering elementWhile the request is in flight
htmx-swappingTarget elementJust before the swap happens
htmx-settlingTarget elementJust after the swap, during settle delay
htmx-addedNewly inserted elementRight after insertion (removed after settling)

All of these classes are CSS hooks. You do not manage them with JavaScript — you write CSS rules that respond to them.

Key Takeaway

HTMX loading indicators rely on the htmx-indicator class and the htmx-request class applied automatically during requests. Place indicators inside the trigger element or point to them with hx-indicator. Style them with CSS to show spinners, progress bars, or overlays. Use CSS on button.htmx-request to disable buttons during requests and block double submissions. The entire system works without writing a single line of JavaScript.

Leave a Comment

Your email address will not be published. Required fields are marked *