htmx Tutorial · User Feedback · Lesson 11 of 30
Loading Indicators
Show a spinner while a request runs with hx-indicator and the htmx-request class, and disable buttons with hx-disabled-elt.
Why indicators matter
Real requests take time. Without feedback, users click again (sending duplicate requests) or think the page is broken. htmx makes loading states almost free.
The htmx-request class
While a request is running, htmx adds the class htmx-request to the element that made it. Any element with the
class htmx-indicator inside it becomes visible during the request (htmx includes the CSS: indicators have
opacity: 0 normally and opacity: 1 during requests).
hx-indicator="#spinner" points to an indicator somewhere else on the page.
<style>.spinner { display: inline-block; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }</style>
<button hx-get="/report" hx-target="#report">
Load report <span class="htmx-indicator spinner">⏳</span>
</button>
<div id="report"></div>
<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
server.get("/report", () => "<p>Sales are up 12% this month.</p>", { delay: 1500 });
</script>
Disabling elements during a request
hx-disabled-elt adds the disabled attribute to elements while the request runs, which stops double submissions:
hx-disabled-elt="this": the element itself.hx-disabled-elt="find button": the button inside a form.- Any CSS selector, or several separated by commas.
<form hx-post="/pay" hx-target="#result" hx-indicator="#saving" hx-disabled-elt="find button">
<input name="amount" value="25">
<button>Pay now</button>
<span id="saving" class="htmx-indicator">Processing payment...</span>
</form>
<p id="result"></p>
<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
server.post("/pay", (req) => `Paid $${server.escape(req.body.amount)}. (Try clicking twice: the button is disabled.)`, { delay: 2000 });
</script>
Preventing overlapping requests
hx-sync decides what happens when a new request starts while another one from the same place is still running:
hx-sync="this:replace" cancels the old one (good for search boxes), this:drop ignores the new one (the default).
Test yourself
Which class does htmx add to the requesting element while its request is in flight?
htmx-requesthtmx-loadinghtmx-indicatoris-busy
Show answer
Answer: htmx-request
Elements with htmx-indicator inside it (or named by hx-indicator) are shown while htmx-request is present.