Skip to content

HTMX Indicators — Complete Guide with Examples

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you'll learn about HTMX indicators. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

HTMX indicators provide visual feedback during AJAX requests, using CSS class toggling and the hx-indicator attribute to show loading states, spinners, and progress animations.

What You'll Learn

By the end of this tutorial, you'll use hx-indicator for custom loading elements, style the htmx-request class, create CSS-only loading animations, build progress bars, and combine indicators with triggers.

Why It Matters

Users need to know their action is being processed. Without indicators, they may click again, navigate away, or think the site is broken. Good indicators improve perceived performance and reduce user frustration.

Real-World Use

Durga Antivirus Pro's scan button shows a pulsing indicator during file analysis. The button text changes to "Scanning..." and a spinner appears next to it. When complete, the indicator disappears and results appear.

Where This Fits in Your Learning Path

flowchart LR
    A["History Management"] --> B["**Indicators & UX**"]
    B --> C["Hyperscript"]
    C --> D["Extensions"]
    D --> E["Advanced HTMX"]
    style B fill:#3b82f6,stroke:#2563eb,color:#fff
    style A fill:#e2e8f0,stroke:#94a3b8
    style E fill:#e2e8f0,stroke:#94a3b8

Basic Indicator with hx-indicator

Point to an element that shows/hides during the request.

<button hx-get="/api/slow-data"
        hx-target="#result"
        hx-indicator="#spinner">
  Load Data
</button>
<img id="spinner" class="htmx-indicator" src="/spinner.gif" alt="Loading..." style="display:none">

Expected output: The spinner is hidden by default. During the request, it becomes visible. When complete, it hides again.

Using the htmx-request CSS Class

The triggering element gets the .htmx-request class during the request.

<button hx-post="/api/submit" class="btn-submit">
  <span class="btn-text">Submit</span>
  <span class="btn-spinner htmx-indicator">Loading...</span>
</button>

<style>
  .btn-submit .btn-spinner { display: none; }
  .btn-submit.htmx-request .btn-text { display: none; }
  .btn-submit.htmx-request .btn-spinner { display: inline; }
</style>

Expected output: The button text is replaced by "Loading..." during the request. The htmx-request class toggles visibility.

CSS-Only Spinner Animation

Create a spinner without external images.

<button hx-get="/api/data" hx-indicator="#css-spinner">
  Load
</button>
<div id="css-spinner" class="htmx-indicator spinner"></div>

<style>
  .spinner {
    display: none;
    width: 24px;
    height: 24px;
    border: 3px solid #e0e0e0;
    border-top-color: #3b82f6;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
  }
  .spinner.htmx-request { display: inline-block; }
  @keyframes spin { to { transform: rotate(360deg); } }
</style>

Expected output: A rotating CSS spinner appears during the request and disappears when complete.

Progress Bar Indicator

Show a horizontal progress bar during requests.

<div hx-post="/api/upload"
     hx-indicator="#progress-container">
  <button type="submit">Upload</button>
</div>

<div id="progress-container" class="htmx-indicator progress-bar" style="display:none">
  <div class="progress-fill"></div>
</div>

<style>
  .progress-bar {
    width: 100%;
    height: 6px;
    background: #e0e0e0;
    border-radius: 3px;
    overflow: hidden;
    margin-top: 8px;
  }
  .progress-fill {
    height: 100%;
    background: #3b82f6;
    width: 30%;
    border-radius: 3px;
    animation: progress-indeterminate 2s ease-in-out infinite;
  }
  .progress-bar.htmx-request { display: block; }
  @keyframes progress-indeterminate {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(400%); }
  }
</style>

Expected output: An animated indeterminate progress bar appears during the request.

Multiple Indicators

Use multiple indicators for different elements.

<div hx-get="/api/dashboard"
     hx-target="#dashboard"
     hx-indicator=".loading-spinner">
  <button class="refresh-btn">Refresh Dashboard</button>
</div>

<div id="dashboard">
  <div class="loading-spinner htmx-indicator" style="display:none">
    Loading dashboard...
  </div>
  Dashboard content here
</div>

Expected output: Multiple elements can share or have separate indicators. The indicator classes control visibility.

Disabling Elements During Request

Use htmx-request class to disable buttons visually and functionally.

<button hx-post="/api/submit-order"
        class="submit-btn"
        aria-busy="false">
  Place Order
</button>

<style>
  .submit-btn.htmx-request {
    opacity: 0.6;
    pointer-events: none;
    cursor: not-allowed;
  }
  .submit-btn.htmx-request::after {
    content: '...';
  }
</style>

Expected output: The button appears disabled during the request, with reduced opacity and a "..." suffix.

Common Mistakes

1. Forgetting the .htmx-indicator class

HTMX looks for the .htmx-indicator class to show/hide elements. Without it, the indicator remains visible.

2. Using display:none with CSS animations

display:none prevents animations. Use opacity or visibility for animated indicators.

3. Not resetting indicator state on error

If a request fails, the indicator remains visible. HTMX handles this automatically, but custom error handling should also hide indicators.

4. Overusing indicators for fast requests

Requests under 200ms don't need indicators. They flash and annoy users. Add a minimum display delay.

5. Creating too many simultaneous indicators

Multiple indicators on the same page can be distracting. Use one global indicator for non-critical requests.

Practice Questions

  1. What attribute shows an indicator during HTMX requests? hx-indicator with a CSS selector pointing to the indicator element.

  2. What CSS class does HTMX add during a request? The .htmx-request class is added to the triggering element.

  3. What CSS class must an indicator have? The .htmx-indicator class for HTMX to manage its visibility.

  4. How do you create a CSS-only spinner? Use a div with a border, border-top-color, border-radius: 50%, and a rotation animation.

  5. Can you disable a button during an HTMX request? Yes. Use CSS on .htmx-request to set pointer-events: none and reduced opacity.

Challenge

Build a submit button that shows an inline spinner, disables itself, changes text to "Saving...", and shows a checkmark when complete using CSS transitions.

FAQ

Can I use multiple indicators for one request?

Yes. Separate selectors with commas in hx-indicator or use the .htmx-request class on multiple elements.

Does hx-indicator work with hx-trigger='load'?

Yes. The indicator shows when the load trigger fires the request.

Can I animate the indicator appearing?

Yes. Use CSS transitions on opacity or transform instead of display:none for smooth appearance.

How do I show a global page-level indicator?

Place an indicator element at the top of the page and reference it with hx-indicator on the body or a wrapper div.

What happens to indicators on error?

HTMX hides indicators on both success and error responses. They only remain if custom JavaScript prevents it.


Mini Project

Build a complete loading UI system with three indicator types: a button with inline spinner, a progress bar for the page, and a toast notification for background operations.

<style>
  .btn-loading { position: relative; }
  .btn-loading .spinner { display: none; width: 16px; height: 16px; border: 2px solid #fff; border-top-color: transparent; border-radius: 50%; animation: spin 0.6s linear infinite; margin-right: 8px; vertical-align: middle; }
  .btn-loading.htmx-request .spinner { display: inline-block; }
  .btn-loading.htmx-request .btn-label { display: none; }

  .page-loader { position: fixed; top: 0; left: 0; width: 100%; height: 3px; z-index: 9999; }
  .page-loader .fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #8b5cf6); animation: load 2s ease-in-out infinite; width: 30%; border-radius: 3px; transform-origin: left; }
  .page-loader.htmx-request .fill { animation: load 1.5s ease-in-out infinite; }
  .page-loader:not(.htmx-request) .fill { width: 0; animation: none; transition: width 0.3s; }

  @keyframes spin { to { transform: rotate(360deg); } }
  @keyframes load { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } }
</style>

<div class="page-loader htmx-indicator"><div class="fill"></div></div>

<button hx-post="/api/save" hx-target="#result" class="btn-loading">
  <span class="spinner htmx-indicator"></span>
  <span class="btn-label">Save Changes</span>
</button>
<div id="result"></div>

What's Next

Learn about Hyperscript:

Tutorial What You'll Learn
Hyperscript Enhance HTMX behavior with Hyperscript
HTMX Extensions Extend HTMX with community extensions

Related topics: CSS animations and transitions, CSS class toggling techniques.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro