In this article, we’ll create a smooth Spinner to Tick Mark animation using HTML, CSS, and a touch of JavaScript. This interactive effect is perfect for confirming actions like form submissions or loading states — adding both function and polish to your UI.
What You’ll Learn
In this tutorial, you’ll discover how to:
- Create a spinning loader using pure CSS.
- Transform the spinner into a tick mark with smooth transitions.
- Use keyframe animations to handle the spin and tick reveal.
- Combine CSS and JavaScript to trigger animations based on user interaction.
- Enhance user feedback in your UI with minimal code.
This is a practical yet visually engaging way to communicate success states on your website or web app.
HTML
<div class="container">
<div class="spinner-container is-loading" id="spinner-container">
<div class="spinner"></div>
<div class="checkmark-circle">
<div class="checkmark">
<div class="splash"></div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" class="tick-icon">
<path d="M18 34 L28 44 L46 20" />
</svg>
</div>
</div>
</div>
<div class="status">
<div class="status-title" id="status-title">Processing...</div>
<div class="status-description" id="status-description">Please wait while we finish up</div>
</div>
<button id="toggle-button" disabled>Working...</button>
</div>
CSS
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f5f5f5;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
text-align: center;
}
.spinner-container {
position: relative;
width: 80px;
height: 80px;
margin-bottom: 1.5rem;
}
.spinner {
width: 80px;
height: 80px;
border: 4px solid #e0e0e0;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
position: absolute;
top: 0;
left: 0;
opacity: 1;
transition: opacity 0.3s ease;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.checkmark-circle {
width: 80px;
height: 80px;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
opacity: 0;
transition: opacity 0.3s ease;
}
.checkmark {
position: relative;
width: 80px;
height: 80px;
}
.tick-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
width: 64px;
height: 64px;
z-index: 2;
filter: drop-shadow(0 2px 4px rgba(46, 204, 113, 0.5));
}
.tick-icon path {
fill: none;
stroke: #2ecc71;
stroke-width: 6;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 100;
stroke-dashoffset: 100;
}
.splash {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 0;
height: 0;
background: radial-gradient(
circle,
rgba(46, 204, 113, 0.4) 0%,
rgba(46, 204, 113, 0) 70%
);
border-radius: 50%;
opacity: 0;
z-index: 1;
}
.checkmark svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
width: 60px;
height: 60px;
z-index: 2;
}
.checkmark path {
fill: none;
stroke: #2ecc71;
stroke-width: 6;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 100;
stroke-dashoffset: 100;
}
@keyframes splash {
0% {
width: 0;
height: 0;
opacity: 0.8;
background: radial-gradient(
circle,
rgba(46, 204, 113, 0.3) 0%,
rgba(46, 204, 113, 0) 70%
);
}
70% {
width: 85px;
height: 85px;
opacity: 0.4;
background: radial-gradient(
circle,
rgba(46, 204, 113, 0.4) 0%,
rgba(46, 204, 113, 0) 70%
);
}
100% {
width: 80px;
height: 80px;
opacity: 0.3;
background: radial-gradient(
circle,
rgba(46, 204, 113, 0.4) 0%,
rgba(46, 204, 113, 0) 70%
);
}
}
@keyframes draw {
0% {
stroke-dashoffset: 100;
stroke: #3498db;
}
60% {
stroke: #2ecc71;
}
100% {
stroke-dashoffset: 0;
stroke: #2ecc71;
}
}
@keyframes pop {
0% {
transform: translate(-50%, -50%) scale(0);
}
50% {
transform: translate(-50%, -50%) scale(1.2);
}
80% {
transform: translate(-50%, -50%) scale(0.9);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
}
.status {
margin-bottom: 1rem;
}
.status-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #333;
}
.status-description {
color: #666;
font-size: 0.875rem;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.is-loading .spinner {
opacity: 1;
}
.is-loading .checkmark-circle {
opacity: 0;
}
.is-success .spinner {
opacity: 0;
}
.is-success .checkmark-circle {
opacity: 1;
}
.is-success .splash {
animation: splash 0.6s ease-out forwards;
}
.is-success .checkmark svg {
animation: pop 0.5s ease 0.2s forwards;
}
.is-success .checkmark path {
animation: draw 0.6s ease 0.4s forwards;
}
JavaScript
const container = document.getElementById("spinner-container");
const statusTitle = document.getElementById("status-title");
const statusDescription = document.getElementById("status-description");
const toggleButton = document.getElementById("toggle-button");
let isLoading = true;
function toggleState() {
if (isLoading) {
container.classList.remove("is-loading");
container.classList.add("is-success");
statusTitle.textContent = "Complete!";
statusDescription.textContent = "Your action was successful";
toggleButton.textContent = "Reset Animation";
toggleButton.disabled = false;
isLoading = false;
} else {
container.classList.remove("is-success");
container.classList.add("is-loading");
statusTitle.textContent = "Processing...";
statusDescription.textContent = "Please wait while we finish up";
toggleButton.textContent = "Working...";
toggleButton.disabled = true;
isLoading = true;
setTimeout(toggleState, 2000);
}
}
toggleButton.addEventListener("click", function () {
if (!isLoading) toggleState();
});
setTimeout(toggleState, 2000);
This spinner-to-checkmark animation is lightweight, flexible, and a great way to visually confirm progress or success. Use it in forms, buttons, or loading screens to enhance user experience with a polished, modern touch!