Skip to content
Knsept
Free playbook

Loading-state button

A submit button that shows a spinner while it works, then success or an error, without changing width.

JavaScriptUI Widgets Updated Oct 23, 2026
Live demo Reduced motion is on, so effects are toned down
<div class="load-demo">
  <button class="load-button" type="button" data-state="idle">
    <span class="load-text" data-for="idle">Save changes</span>
    <span class="load-text" data-for="busy"><span class="spinner" aria-hidden="true"></span>Saving…</span>
    <span class="load-text" data-for="done">Saved ✓</span>
    <span class="load-text" data-for="failed">Try again</span>
  </button>
  <label class="load-option"><input type="checkbox" id="fail-next"> Make the next save fail</label>
  <p class="load-status" role="status"></p>
</div>

All four labels live in the same grid cell, and only the active one is visible. That makes the button as wide as its longest label, so it never changes size when the state changes. Changing width mid-click is a classic cause of mis-taps.

The whole state machine is one attribute, data-state, which CSS reads to pick the label and color. The click handler ignores presses while busy, so double clicks don't send two requests. aria-busy and a role="status" line keep screen-reader users informed.

Use it for real: replace fakeSave() with your fetch() call.

More from the library

Feedback live

Confetti burst

A celebration burst from a button, drawn on a canvas with simple physics. Under 60 lines, no library.

Feedback live

Progress ring

A circular progress indicator from one SVG circle and stroke-dasharray, with an accessible progressbar role.

Feedback live

Skeleton loader

Shimmering placeholders that hold the layout while content loads, so nothing jumps when it arrives.