<h1>
  CSS Button Pending / Success / Fail Animation
</h1>

<p>
  Nice animated button I've created together with <a href="http://szyperrek.com/">Alexander Szyperrek</a> for <a href="http://mytaxi.com">mytaxi</a>. To give you a better understanding of whats going on, theese are the states we're going to animate between:
</p>
<p class="state-list">
  <span class="loading-btn-wrapper">
    <button class="loading-btn">
      <span class="loading-btn__text">
        Submit  
      </span>
    </button>
  </span>

  <span class="loading-btn-wrapper">
    <button class="loading-btn loading-btn--pending">
      <span class="loading-btn__text">
        Submit  
      </span>
    </button>
  </span>

  <span class="loading-btn-wrapper">
    <button class="loading-btn loading-btn--success">
      <span class="loading-btn__text">
        Submit  
      </span>
    </button>
  </span>
  
<span class="loading-btn-wrapper">
    <button class="loading-btn loading-btn--fail">
      <span class="loading-btn__text">
        Submit  
      </span>
    </button>
  </span>
</p>


<p>
  Click button below to see the states in an sequenced animation:
</p>

<span class="loading-btn-wrapper">
  <button class="loading-btn js_success-animation-trigger">
    <span class="loading-btn__text">
      Submit
    </span>
  </button>
</span>

<span class="loading-btn-wrapper">
  <button class="loading-btn js_fail-animation-trigger">
    <span class="loading-btn__text">
      Submit  
    </span>
  </button>
</span>
// Config
$circle-size: 40px;

// Unfortunatly we need a wrapper element containing the fixed width for centering the button within the animtion (you could also apply the width as an inline style).
.loading-btn-wrapper {
  display: inline-block;
  width: 240px;
  height: $circle-size;
  
  text-align: center;
}

.loading-btn {
  $root: &;
  
  position: relative;
  
  display: inline-block;
  width: 100%;
  height: 100%;
  
  background: #03a9f4;
  
  border: 0;
  border-radius: 24px;
  
  cursor: pointer;
  
  transition: all .33s ease-in-out;
  
  &:hover {
    background: #2196f3;
  }
  
  &, &:focus {
    outline: none;
  }
  
  // Styles for all states
  &--pending,
  &--success,
  &--fail {
    // Morph button to circle (width equals height)
    width: $circle-size;
    
    // Prevent any further clicks triggering events during animation
    pointer-events: none;
    cursor: default;
    
    // Hide text
    #{$root}__text {
      opacity: 0;
    }
  }
  
  // State "pending"
  // Show loading indicator
  &--pending:before {
    content: '';

    position: absolute;
    top: 50%;
    left: 50%;

    display: inline-block;

    // Can't use percentage here as we already show this icon during morph animation
    height: #{$circle-size * .7};
    width: #{$circle-size * .7};

    border: 3px solid rgba(255, 255, 255, .33);
    border-top-color: #fff;
    border-radius: 50%;

    animation:
      loading-btn--fade-in .33s ease,
      loading-btn--rotation .66s linear 0s infinite;
  }
    
  // Success state - show check icon
  &--success {
    
    // Different background color (also on hover)
    &, &:hover {
      background: #8bc34a;
    }

    // Use "after" pseudo to trigger new fade in animation, as "before" is already used on "--pending"
    &:after {
      content: '';

      position: absolute;
      top: 50%;
      left: 50%;

      // Simulate checkmark icon
      display: inline-block;
      height: 25%;
      width: 50%;

      border: 3px solid #fff;
      border-top-width: 0;
      border-right-width: 0;

      transform: translate(-50%, -75%) rotate(-45deg);

      animation: 
        loading-btn--fade-in .6s ease;
    }
  } 

  // Fail state - show cross icon
  &--fail {
    
    // Different background color (also on hover)
    &, &:hover {
      background: #ff5722;
    }

    // Use "after" pseudo to trigger new fade in animation, as "before" is already used on "--pending"
    &:after {
      content: '';

      position: absolute;
      top: 50%;
      left: 50%;

      // Simulate cross icon
      display: inline-block;
      height: 65%;
      width: 65%;

      // Using background gradient is the only solution creating a cross with a single element
      background: 
        linear-gradient(
          to bottom,
          transparent 44%, 
          #fff 44%, 
          #fff 56%,  
          transparent 56%
        ),

        linear-gradient(
          to right,
          transparent 44%, 
          #fff 44%, 
          #fff 56%,  
          transparent 56%
      );

      transform: translate(-50%, -50%) rotate(-45deg);

      animation: 
        loading-btn--fade-in .6s ease;
    }
  } 
  
  // Text has to be positioned absolute in order prevent line-breaks or trimming of text when morphing button to circle.  
  &__text {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    
    font-size: 13px;
    color: #fff;
    
    transition: inherit;
  }
}


/**
 * Animations
 */

@keyframes loading-btn--fade-in {
    0% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

@keyframes loading-btn--rotation {
    0% {
        transform: translate(-50%, -50%) rotate(0deg);
    }

    100% {
        transform: translate(-50%, -50%) rotate(360deg);
    }
}


/**
 * Optical stuff - has nothing todo with button animation.
 */

.state-list {
  margin-bottom: 12px;
  
  .loading-btn-wrapper {
    background: repeating-linear-gradient(
      45deg,
      #fff,
      #fff 10px,
      #f0f0f0 10px,
      #f0f0f0 20px
    );
  }
}

.loading-btn-wrapper {
  & + & {
    margin-left: 8px;
  }
}
View Compiled
const successBtnElement = document.querySelector('.js_success-animation-trigger');
const failBtnElement    = document.querySelector('.js_fail-animation-trigger');

const pendingClassName = 'loading-btn--pending';
const successClassName = 'loading-btn--success';
const failClassName    = 'loading-btn--fail';

const stateDuration = 1500;

successBtnElement.addEventListener('click', (ev) => {
  const elem = ev.target;
  elem.classList.add(pendingClassName);
  
  window.setTimeout(() => {
      elem.classList.remove(pendingClassName);
      elem.classList.add(successClassName);
    
      window.setTimeout(() => elem.classList.remove(successClassName), stateDuration);
  }, stateDuration);
});

failBtnElement.addEventListener('click', (ev) => {
  const elem = ev.target;
  elem.classList.add(pendingClassName);
  
  window.setTimeout(() => {
      elem.classList.remove(pendingClassName);
      elem.classList.add(failClassName);
    
      window.setTimeout(() => elem.classList.remove(failClassName), stateDuration);
  }, stateDuration);
});
View Compiled

External CSS

  1. https://codepen.io/fxm90/pen/gMwRyy.scss

External JavaScript

This Pen doesn't use any external JavaScript resources.