<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
This Pen doesn't use any external JavaScript resources.