You can create a spinner by animating the stroke properties of a SVG circle element.
See the Pen SVG circle spinner (css keyframes only) by IpsumLorem16 (@ipsumlorem16) on CodePen.
If you are new to SVG animation this post would be a good place to start: An Introduction to SVG Animation
How it’s made
The two main parts of the SVG, are a couple of circle
elements on top of each other, both with transparent fill and 6px stroke. The .ring-track
is static and just sits behind the spinner for decoration, while the spinner .loader-ring
rotates and changes length.
This might seem a bit confusing if you are unfamiliar with this method, here is useful article by CSS tricks, that will help you understand how svg-line-animation-works
<circle
class="ring-track"
fill="transparent"
stroke-width="6px"
stroke="#9c9c9c30"
cx="50" cy="50"
r="44"
/>
<circle
class="loader-ring"
fill="transparent"
stroke-width="6px"
stroke="#ec5c0e"
stroke-dashoffset="276.460"
stroke-dasharray="276.460 276.460"
cx="50"
cy="50"
r="44"
/>
Setting the size of the stroke-dasharray
to the same radius of .loader-ring
(or larger) completely covers it with one long dash. The second value adds a gap of equal length. So it appears you have the outline of a circle.
And setting stroke-dashoffset
to the same value, pushes the dash along so it is no longer visible.
Animating
The idea is simple, vary the width of .loader-ring
, while also rotating it clockwise.
stroke-dashoffset
To vary the width of the spinner .loader-ring
you can set the stroke property dash-offset
.
Full-width would be stroke-dashoffset="0"
, and fully hidden stroke-dashoffset="276.460"
.
So I created two keyframe animations, one to first ‘load’ the spinner into view: starting-fill
, and one to ‘randomly’ vary the width of it: vary-loader-width
svg .loader-ring {
animation:
starting-fill 0.5s forwards,
vary-loader-width 3s 0.5s linear infinite alternate;
}
@keyframes starting-fill {
to {
stroke-dashoffset: 270;
}
}
@keyframes vary-loader-width {
0% {
stroke-dashoffset: 270;
}
50% {
stroke-dashoffset: 170;
}
100% {
stroke-dashoffset: 275;
}
}
The vary-loader-width
animation will play forwards then backwards forever, but only after starting fill
has completed, because of the 0.5s delay added.
Spinning
To get the SVG circle
element spinning on the right axis you first need to set the correct transform-origin
. This needs to be XY co-ordinates in pixels from the top left of the SVG. For the circle which is dead center, it is transform-origin: 50px 50px
, same as it’s cx="50" cy="50"
position attributes.
Now it can be rotated without it going wonky, lets add that animation onto it:
svg .loader-ring {
transform-origin: 50px 50px;
animation:
starting-fill 0.5s forwards,
vary-loader-width 3s 0.5s linear infinite alternate,
spin 1.6s 0.2s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
That’s it, for the basic animation. Getting it going was the easy part. What took me a while to get right was tweaking timings and stroke-dashoffset
amounts to make it look somewhat interesting. I used a trial and error approach, but it might be easier to study other animations to see how they move.
Adding more
You might have noticed in the pen you can click the spinner to ‘finish loading’. Where it fills up entirely, and then fades away.
Adding this effect, I could not find an easy way to smoothly transition the current width of the .loader-ring
to full width. It would always quickly shrink back down to zero before completing, not ideal.
A hacky solution was to add a duplicate spinner .loader-ring-overlay
that is hidden until needed. Filling the circle and then fading out the svg. That turned out pretty smooth.
<circle
class="loader-ring-overlay"
fill="transparent"
stroke-width="6px"
stroke="#ec5c0e"
stroke-dashoffset="276.460"
stroke-dasharray="276.460 276.460"
cx="50"
cy="50"
r="44"
/>
As you can see it is clone of the other circle, but currently hidden, and with only the spin animation running (so it is in sync with .loader-ring
).
svg .loader-ring-overlay {
visibility: hidden;
transform-origin: 50px 50px;
animation: spin 1.6s 0.2s linear infinite;
}
In this demonstration when the spinner is clicked, the class .complete
is added to a parent. That ‘finishes loading’ the loader, and fades it out. A timeout is also triggered to run when it is complete, removing the SVG from the DOM.
const loader = document.querySelector("svg");
// on clicking svg spinner, 'finish loading'
loader.addEventListener("click", e => {
document.body.classList.add("complete");
setTimeout(() => {
loader.classList.add("hidden");
button.classList.remove('hidden');
}, 900);
});
.complete .loader-ring-overlay {
visibility: visible;
animation:
complete-fill 0.5s linear forwards,
spin 1.6s 0.2s linear infinite;
}
.complete .loader-ring {
animation:
starting-fill 0.5s forwards,
vary-loader-width 3s 0.5s linear infinite alternate,
spin 1.6s 0.2s linear infinite, fade 0.1 0.5s linear forwards;
}
.complete svg {
animation: fade 0.2s 0.7s linear forwards;
transition: all 0s 0.9s;
cursor: initial;
pointer-events: none;
}
I also added a reset button, that fades in, so you can play it over and over.