No animations match your search in this category.
The code for all 8 loaders, explained
Every effect above, with the markup to use and the complete CSS. Each one is self-contained: paste the CSS once, add the class, done.
Ring Spinner
The classic. One element, four lines of keyframes. Add role="status" and an aria-label so screen readers announce it.
Markup
<div class="fh-spinner" role="status" aria-label="Loading"></div>
CSS
.fh-spinner {
width: 44px;
height: 44px;
border: 4px solid rgba(139, 92, 246, 0.2);
border-top-color: #8b5cf6;
border-radius: 50%;
animation: fh-spin 0.8s linear infinite;
}
@keyframes fh-spin {
to { transform: rotate(360deg); }
}
Dual Ring
Two opposing arcs spinning together. Colour the top and bottom borders with your two brand colours.
Markup
<div class="fh-dual-ring" role="status" aria-label="Loading"></div>
CSS
.fh-dual-ring {
width: 44px;
height: 44px;
border: 4px solid transparent;
border-top-color: #8b5cf6;
border-bottom-color: #06b6d4;
border-radius: 50%;
animation: fh-spinDual 1s linear infinite;
}
@keyframes fh-spinDual {
to { transform: rotate(360deg); }
}
Bouncing Dots
The “someone is typing” pattern. The staggered animation-delay on each dot creates the wave.
Markup
<div class="fh-dots-bounce" role="status" aria-label="Loading">
<span></span><span></span><span></span>
</div>
CSS
.fh-dots-bounce {
display: flex;
gap: 8px;
align-items: flex-end;
}
.fh-dots-bounce span {
width: 12px;
height: 12px;
border-radius: 50%;
background: #06b6d4;
animation: fh-dotBounce 0.6s ease-in-out infinite alternate;
}
.fh-dots-bounce span:nth-child(2) { animation-delay: 0.15s; }
.fh-dots-bounce span:nth-child(3) { animation-delay: 0.3s; }
@keyframes fh-dotBounce {
from { transform: translateY(0); opacity: 0.6; }
to { transform: translateY(-14px); opacity: 1; }
}
Pulsing Dots
Calmer than bouncing dots — good inside buttons while a form submits.
Markup
<div class="fh-dots-pulse" role="status" aria-label="Loading">
<span></span><span></span><span></span>
</div>
CSS
.fh-dots-pulse {
display: flex;
gap: 8px;
}
.fh-dots-pulse span {
width: 12px;
height: 12px;
border-radius: 50%;
background: #8b5cf6;
animation: fh-dotPulse 1s ease-in-out infinite;
}
.fh-dots-pulse span:nth-child(2) { animation-delay: 0.2s; }
.fh-dots-pulse span:nth-child(3) { animation-delay: 0.4s; }
@keyframes fh-dotPulse {
0%, 100% { transform: scale(0.6); opacity: 0.4; }
50% { transform: scale(1); opacity: 1; }
}
Indeterminate Bar
For when you can’t know the progress percentage. A gradient segment sweeps the track endlessly.
Markup
<div class="fh-bar-loader" role="progressbar" aria-label="Loading"></div>
CSS
.fh-bar-loader {
width: 160px;
height: 6px;
border-radius: 3px;
background: rgba(139, 92, 246, 0.15);
overflow: hidden;
position: relative;
}
.fh-bar-loader::before {
content: '';
position: absolute;
top: 0; left: 0;
height: 100%;
width: 40%;
border-radius: 3px;
background: linear-gradient(90deg, #8b5cf6, #06b6d4);
animation: fh-barSlide 1.2s ease-in-out infinite;
}
@keyframes fh-barSlide {
0% { left: -40%; }
100% { left: 100%; }
}
Skeleton Shimmer
Skeleton screens make loads feel faster than spinners because they preview the page structure. Size each bar like the content it replaces.
Markup
<div class="fh-skeleton" style="width: 180px;"></div>
<div class="fh-skeleton" style="width: 120px;"></div>
CSS
.fh-skeleton {
width: 180px;
height: 16px;
border-radius: 6px;
background: linear-gradient(90deg,
rgba(148, 163, 184, 0.12) 25%,
rgba(148, 163, 184, 0.28) 50%,
rgba(148, 163, 184, 0.12) 75%);
background-size: 200% 100%;
animation: fh-shimmer 1.4s linear infinite;
}
@keyframes fh-shimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
Ripple
Two expanding rings offset by half the duration. Feels organic, like drops hitting water.
Markup
<div class="fh-ripple" role="status" aria-label="Loading"></div>
CSS
.fh-ripple {
position: relative;
width: 56px;
height: 56px;
}
.fh-ripple::before,
.fh-ripple::after {
content: '';
position: absolute;
inset: 0;
border: 3px solid #06b6d4;
border-radius: 50%;
animation: fh-rippleWave 1.6s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.fh-ripple::after {
animation-delay: 0.8s;
}
@keyframes fh-rippleWave {
from { transform: scale(0.1); opacity: 1; }
to { transform: scale(1); opacity: 0; }
}
Orbit
A satellite circling a dashed orbit. The dot is a pseudo-element, so it is still just one div.
Markup
<div class="fh-orbit" role="status" aria-label="Loading"></div>
CSS
.fh-orbit {
position: relative;
width: 52px;
height: 52px;
border-radius: 50%;
border: 2px dashed rgba(139, 92, 246, 0.35);
animation: fh-spinOrbit 2.4s linear infinite;
}
.fh-orbit::before {
content: '';
position: absolute;
top: -7px;
left: 50%;
width: 12px;
height: 12px;
margin-left: -6px;
border-radius: 50%;
background: #06b6d4;
}
@keyframes fh-spinOrbit {
to { transform: rotate(360deg); }
}
Choosing the right loader for the wait
The type of loader should match how long the wait will be and whether you can measure it. For waits under about a second, showing nothing at all is often best — a spinner that flashes for 300ms reads as a glitch rather than progress.
For short indeterminate waits, a spinner or bouncing dots works. For anything you can measure, use a progress bar: knowing that a task is 70% done is far less stressful than watching an endless spin. And when you're loading content into a known layout, a skeleton screen beats both.
Why skeleton screens feel faster
Skeletons outperform spinners for the same wait because they set expectations. A spinner says "something is happening somewhere"; a skeleton shows the shape of what's arriving, so the eye already knows where the headline and the image will land. When the content appears, it slots into place instead of replacing an unrelated animation.
The rule that makes them work: size each placeholder bar like the content it stands in for. Skeletons that don't match the final layout cause a jump when the real content loads — which is exactly the layout shift you were trying to avoid.
Accessibility: loaders need to be announced
A purely visual loader is invisible to a screen reader user, who gets silence and no indication anything is happening. Give the element role="status" and an aria-label such as "Loading results", and assistive technology will announce it politely without interrupting.
Also respect prefers-reduced-motion. Continuously spinning elements can trigger discomfort for people with vestibular disorders — a static or fading indicator is a reasonable substitute, and the reduced-motion snippet in our how-to guide handles this globally.
Keeping loaders cheap
Every loader here animates only transform, opacity or background-position, which browsers composite on the GPU. That matters more than usual for loaders: they run precisely when the main thread is busy fetching and parsing, so an animation that requires layout work will stutter exactly when it's most visible.