Submit Button with Pure CSS Loading Spinner

Many forms and apps will include a submit button (or similar) that, when clicked, will become temporarily disabled while some action is taking place. During the wait time, a good UI practice is to insert an animated loading indicator. This can be done with pure CSS while the JavaScript is just used to enable/disable the button while also performing some asynchronous action in the process. This could also be used when a general form submission is done synchronously.

Here is the HTML for the button:

<button>SUBMIT FORM<span class="spinner"></span></button>

Here is the CSS (commented):

/* This is the submit button styles */
button {
  display: block;
  margin: 0 auto;
  padding: .6em .8em;
  /* Font-size is the root value that determines size of spinner parts. 
Change this to whatever you want and spinner elements will size to match. */
  font-size: 20px;
  font-weight: bold;
  border-radius: .4em;
  border: none;
  overflow: hidden;
  cursor: pointer;
  position: relative;
  transition: all 1s;
}

/* focus/disabled styles, you can change this for accessibility */
button:focus, button:disabled {
  outline: none;
  background: #aaa;
}

/* This is the space for the spinner to appear, applied to the button */
.spin {
  padding-left: 2.5em;
  display: block;
}

/* position of the spinner when it appears, you might have to change these values */
.spin .spinner {
  left: -.6em;
  top: .4em;
  width: 2.5em;
  display: block;
  position: absolute;
}

/* spinner animation */
@keyframes spinner {
  0% {
    transform: rotate(0deg);
  }
  
  100% {
    transform: rotate(360deg);
  }
}

/* The actual spinner element is a pseudo-element */
.spin .spinner::before {
  content: "";
  width: 1.5em; /* Size of the spinner */
  height: 1.5em; /* Change as desired */
  position: absolute;
  top: 50%;
  left: 50%;
  border-radius: 50%;
  border: solid .35em #999; /* Thickness/color of spinner track */
  border-bottom-color: #555; /* Color of variant spinner piece */
  animation: .8s linear infinite spinner; /* speed of spinner */
  transform: translate(-50%, -50%);
  will-change: transform;
}

/* optional, but it will affect the size if changed */
*, *::before, *::after {
  box-sizing: border-box;
}

Finally, the JavaScript:

let btn = document.querySelector('button');

btn.addEventListener('click', function () {
  // form submission starts
  // button is disabled
  btn.classList.add('spin');
  btn.disabled = true;
  
  // This disables the whole form via the fieldset
  btn.form.firstElementChild.disabled = true;
  
  // this setTimeout call mimics some asyncronous action
  // you would have something else here
  window.setTimeout(function () {
    // when asyncronous action is done, remove the spinner
    // re-enable button/fieldset
    btn.classList.remove('spin');
    btn.disabled = false;
    btn.form.firstElementChild.disabled = false;
  }, 4000);
}, false);

And here is a live demo:

Some things worth noting:

  • The JavaScript uses a setTimeout() call to mimic what might happen using Ajax. This would be removed and replaced with whatever you code is doing during the button submission process.
  • The size of the spinner is based on the font-size set on the <button> element using pixels. Adjust this and the spinner size and position will change automatically to match via em units.
  • The button also disables the parent fieldset. This is optional, but it’s an added feature. You can disable this by removing the appropriate commented line in the code.
  • The HTML in the demo disables the form from being submitted. This is only for demo purposes.

Related: Ladda spinners by Hakim El Hattab

Note: To the best of our knowledge, the information above and the snippet are accurate and up to date. However, in case you notice something wrong, please report snippet or leave a comment below.
Back to Snippets
Scroll to Top