BUTTONS

Loading Button

An interactive loading button that shows a spinner while processing and displays a success state when complete. Copy the HTML, CSS and JavaScript for your own project.

HTML
<button class="loading-btn" type="button">
  <span class="loading-spinner"></span>
  <span class="loading-text">Submit</span>
</button>
CSS
.loading-btn{
  min-width:140px;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:9px;
  padding:15px 30px;
  border:0;
  border-radius:10px;
  background:#8b5cf6;
  color:#ffffff;
  font-size:15px;
  font-weight:700;
  cursor:pointer;
  transition:
    transform .25s ease,
    background .25s ease,
    box-shadow .25s ease;
}

.loading-btn:hover{
  transform:translateY(-2px);
  box-shadow:0 10px 28px rgba(139,92,246,.35);
}

.loading-spinner{
  display:none;
  width:16px;
  height:16px;
  border:2px solid rgba(255,255,255,.35);
  border-top-color:#ffffff;
  border-radius:50%;
  animation:loadingSpin .7s linear infinite;
}

.loading-btn.is-loading .loading-spinner{
  display:block;
}

.loading-btn.is-loading{
  cursor:wait;
}

.loading-btn.is-done{
  background:#16a34a;
}

@keyframes loadingSpin{
  to{
    transform:rotate(360deg);
  }
}
JAVASCRIPT
const loadingBtn = document.querySelector('.loading-btn');
const loadingText = loadingBtn.querySelector('.loading-text');

loadingBtn.addEventListener('click', function(){

  loadingBtn.classList.add('is-loading');
  loadingText.textContent = 'Loading...';
  loadingBtn.disabled = true;

  setTimeout(function(){

    loadingBtn.classList.remove('is-loading');
    loadingBtn.classList.add('is-done');
    loadingText.textContent = 'Done ✓';

    loadingBtn.disabled = false;

  }, 2000);

});

How This Loading Button Works

This loading button gives users clear visual feedback during an async action — like a form submission or API call — by switching between three distinct states: idle, loading, and success. The HTML keeps the structure simple: one button contains a spinner and a text label. The spinner stays hidden until loading begins, while the text changes to match the current state.

The JavaScript is structured around a click event that triggers a state change sequence. When clicked, it first checks whether the button is already in a loading state (if (loadingBtn.classList.contains('is-loading')) return;) — this guard prevents users from triggering multiple overlapping loading sequences by clicking repeatedly. It then removes any previous “done” state, adds the is-loading class, updates the text to “Loading…”, and disables the button entirely using the native disabled attribute, which prevents further clicks and this also makes it clear that the button is temporarily unavailable while the action is being processed.

For this demo, a 2-second setTimeout simulates the time a real request might take to complete. The script removes the loading class, adds an is-done class, updates the text to “Done ✓”, and re-enables the button. The spinner itself is a simple CSS-only animation — a circular element with one colored border side (border-top-color) that rotates continuously via @keyframes loadingSpin, creating the classic spinning-loader look without any JavaScript-driven animation logic.

Customizing This Loading Button

Several parts of this button’s behavior and appearance can be adjusted:

  • Change the loading duration — the 2000 value (in milliseconds) inside the setTimeout controls how long the button stays in its loading state; replace this with your actual API call logic in a real project.
  • Update the color states — the default background (#8b5cf6) and success background (#16a34a for the “done” state) can be changed in .loading-btn and .loading-btn.is-done respectively.
  • Adjust the spinner speed — the .7s duration in the loadingSpin animation controls how fast the spinner rotates.
  • Change the button text at each stage — the “Submit,” “Loading…,” and “Done ✓” text values are set directly in the JavaScript and HTML, so they can be swapped for wording that fits your specific action.

Where to Use This Loading Button

This pattern is especially useful anywhere a user action triggers a delay before completing:

  • Form submissions, giving users confidence that their click registered while data is being processed.
  • Newsletter or signup forms, where a brief loading state followed by a success confirmation reduces uncertainty.
  • Any button tied to an API call, such as saving settings or submitting a request, where instant feedback prevents users from clicking multiple times.

Disabling the button during the loading state is a small but important detail — it prevents duplicate form submissions or repeated API calls, which is a common source of bugs in real-world applications.

Looking for another interactive button style? Check out our Social CTA Gradient Button for a bold, animated call-to-action design.

Frequently Asked Questions

What happens if the user clicks the button while it’s already loading?
Nothing — the click is ignored. Because the button is both visually and functionally disabled (loadingBtn.disabled = true) during the loading state, and the JavaScript also checks for the is-loading class before running again, there’s no risk of the loading sequence restarting or overlapping.

How would I connect this to a real API call instead of a fake delay?
Replace the setTimeout with your actual fetch or AJAX request. Update the button to its success state only after the request completes successfully. This way, users see “Done ✓” only when the real operation has finished.

Does the button return to its original “Submit” state after showing “Done”?
Not automatically in this version — once marked as done, it stays in that success state until the page reloads or you manually reset it. If you want it to reset after a few seconds, you could add a second setTimeout inside the success callback to remove the is-done class and restore the original text.

Scroll to Top