Loaders & Spinners
Loaders, spinners and skeleton loading for an optimal waiting UX.
Updated
How do you make a loading spinner or skeleton screen in pure CSS?
A loader tells people that something is happening while content arrives. Most are pure CSS: a border or SVG arc rotated with @keyframes, dots scaled one after another with animation-delay, or skeleton screens, grey placeholders shaped like the final content and crossed by a moving shimmer gradient.
How to choose, performance and accessibility
- Prefer a skeleton screen such as Card Skeleton when you know the layout of what is loading, a Classic Spinner or Bouncing Dots for short and unknown waits, and a Progress Bar when progress can be measured.
- Add
role="status"with a text alternative so screen readers announce the wait, animate onlytransformandopacity, and slow things down forprefers-reduced-motion. - Classic Spinner, SVG Morphing Loaders, Bouncing Dots and Card Skeleton are free with their full code; the others come with Premium.
View source code
<div class="demo-preview">
<div class="loader-spinner"></div>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(26, 26, 36);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.loader-spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(99, 102, 241, 0.2);
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}/* Pure CSS — no JavaScript required */SVG Morphing Loaders
Three organic indicators whose shapes morph in a smooth loop (native SVG path interpolation). Zero JavaScript, static fallback under prefers-reduced-motion.
💡 Perfect for: loading states, AI waits, splash screens, processing buttons.
Effect page →View source code
<div class="demo-preview">
<div class="sml-row">
<div class="sml-item"><svg class="sml-anim" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true">
<path fill="url(#smlGradA)">
<animate attributeName="d" dur="2.2s" repeatCount="indefinite" calcMode="spline"
keyTimes="0;.5;1" keySplines=".4 0 .2 1;.4 0 .2 1"
values="M27 5 C41 5 49 13 49 27 C49 41 41 49 27 49 C13 49 5 41 5 27 C5 13 13 5 27 5 Z;M27 8 C44 3 51 16 46 29 C42 44 30 52 18 46 C6 41 2 26 9 16 C14 8 20 10 27 8 Z;M27 5 C41 5 49 13 49 27 C49 41 41 49 27 49 C13 49 5 41 5 27 C5 13 13 5 27 5 Z"/>
</path>
</svg>
<svg class="sml-static" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true"><path fill="url(#smlGradA)" d="M27 5 C41 5 49 13 49 27 C49 41 41 49 27 49 C13 49 5 41 5 27 C5 13 13 5 27 5 Z"/></svg><span class="sml-label">Blob</span></div>
<div class="sml-item"><svg class="sml-anim" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true">
<path fill="#5a9bff">
<animate attributeName="d" dur="1.6s" repeatCount="indefinite" calcMode="spline"
keyTimes="0;.5;1" keySplines=".4 0 .2 1;.4 0 .2 1"
values="M27 6 C34 16 44 24 44 33 C44 43 36 49 27 49 C18 49 10 43 10 33 C10 24 20 16 27 6 Z;M27 10 C38 10 46 18 46 28 C46 39 38 46 27 46 C16 46 8 39 8 28 C8 18 16 10 27 10 Z;M27 6 C34 16 44 24 44 33 C44 43 36 49 27 49 C18 49 10 43 10 33 C10 24 20 16 27 6 Z"/>
</path>
</svg>
<svg class="sml-static" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true"><path fill="#5a9bff" d="M27 6 C34 16 44 24 44 33 C44 43 36 49 27 49 C18 49 10 43 10 33 C10 24 20 16 27 6 Z"/></svg><span class="sml-label">Drop</span></div>
<div class="sml-item"><svg class="sml-anim" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true">
<path fill="#ffd166">
<animate attributeName="d" dur="1.9s" repeatCount="indefinite" calcMode="spline"
keyTimes="0;.5;1" keySplines=".4 0 .2 1;.4 0 .2 1"
values="M27 6 L33 20 L48 21 L37 31 L41 46 L27 38 L13 46 L17 31 L6 21 L21 20 Z;M27 12 L31 22 L42 23 L34 30 L36 41 L27 36 L18 41 L20 30 L12 23 L23 22 Z;M27 6 L33 20 L48 21 L37 31 L41 46 L27 38 L13 46 L17 31 L6 21 L21 20 Z"/>
</path>
</svg>
<svg class="sml-static" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true"><path fill="#ffd166" d="M27 6 L33 20 L48 21 L37 31 L41 46 L27 38 L13 46 L17 31 L6 21 L21 20 Z"/></svg><span class="sml-label">Star</span></div>
<svg width="0" height="0" aria-hidden="true"><defs><linearGradient id="smlGradA" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#7c3aed"/><stop offset="1" stop-color="#db2777"/></linearGradient></defs></svg>
</div>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(26, 26, 36);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.sml-row {
display: flex;
gap: 34px;
align-items: center;
justify-content: center
}
.sml-item {
display: grid;
place-items: center;
gap: 8px
}
.sml-item svg {
display: block
}
.sml-label {
font-size: .62rem;
letter-spacing: .14em;
text-transform: uppercase;
color: #8f8fb0
}
.sml-item .sml-static {
display: none
}
@media (prefers-reduced-motion: reduce) {
.sml-item .sml-anim {
display: none
}
.sml-item .sml-static {
display: block
}
}/* Pure CSS — no JavaScript required */View source code
<div class="demo-preview">
<div class="loader-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(26, 26, 36);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.loader-dots {
display: flex;
gap: 8px;
}
.loader-dots span {
width: 12px;
height: 12px;
background: #6366f1;
border-radius: 50%;
animation: dotBounce 1.4s ease-in-out infinite;
}
.loader-dots span:nth-child(1) {
animation-delay: -0.32s;
}
.loader-dots span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes dotBounce {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}/* Pure CSS — no JavaScript required */Card Skeleton
Classic card placeholder with avatar, title and text lines animated by a sliding shimmer.
Effect page →View source code
<div class="demo-preview">
<div class="card-skel">
<div class="card-skel-header">
<div class="skel-bone-circle card-skel-avatar"></div>
<div style="flex:1">
<div class="skel-bone card-skel-title"></div>
<div class="skel-bone card-skel-subtitle"></div>
</div>
</div>
<div class="skel-bone card-skel-img"></div>
<div class="skel-bone card-skel-line"></div>
<div class="skel-bone card-skel-line"></div>
<div class="skel-bone card-skel-line"></div>
</div>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(26, 26, 36);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.skel-bone {
background: rgba(255, 255, 255, 0.08);
border-radius: 6px;
position: relative;
overflow: hidden;
}
.skel-bone-circle {
background: rgba(255, 255, 255, 0.08);
border-radius: 50%;
position: relative;
overflow: hidden;
}
.skel-bone::after,
.skel-bone-circle::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.1) 50%,
transparent 100%);
background-size: 200% 100%;
animation: shimmerSlide 1.8s ease-in-out infinite;
}
.card-skel {
width: 280px;
padding: 20px;
background: rgba(255, 255, 255, 0.03);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.card-skel-header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 20px;
}
.card-skel-avatar {
width: 48px;
height: 48px;
flex-shrink: 0;
}
.card-skel-title {
height: 14px;
width: 65%;
margin-bottom: 8px;
}
.card-skel-subtitle {
height: 10px;
width: 45%;
}
.card-skel-img {
height: 120px;
width: 100%;
margin-bottom: 16px;
border-radius: 10px;
}
.card-skel-line {
height: 10px;
width: 100%;
margin-bottom: 10px;
}
.card-skel-line:nth-child(2) {
width: 92%;
}
.card-skel-line:last-child {
width: 60%;
margin-bottom: 0;
}
@keyframes shimmerSlide {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}/* Pure CSS — no JavaScript required */Image Grid Skeleton
2x2 grid of image placeholders with staggered shimmer for a cascade effect.
Effect page →Article Skeleton
Blog article skeleton: hero image, title, paragraphs and author info.
Effect page →Pulse Loading
Same card structure but with a pulse/breathing animation instead of classic shimmer.
Effect page →Wave Shimmer
Undulating shimmer that flows like water, with a fluid and organic motion.
Effect page →Rainbow Shimmer
Skeleton with a colorful rainbow gradient shimmer sweeping across elements.
Effect page →Skeleton to Content
Click to toggle between skeleton state and real content with a smooth transition.
Effect page →