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
| Class | Added To | When |
|---|---|---|
| htmx-request | Triggering element | While the request is in flight |
| htmx-swapping | Target element | Just before the swap happens |
| htmx-settling | Target element | Just after the swap, during settle delay |
| htmx-added | Newly inserted element | Right 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.
