Skip to content

Progress Ring Spinners

:state(--indeterminate) scopes the overrides to the spinning state. The built-in motion lives on ::part(fill), so an animation there replaces it, while ::part(svg) adds rotation of the whole ring. stroke-dasharray reshapes the arc against the ring's path length, held in a --path-length variable that custom properties carry into the shadow tree, keyframes included.

Code

The following example assumes that you have imported the component and set up the theme.

Spinners
<div class="spinner-gallery">
29 collapsed lines
<figure>
<sv-progress-ring class="default-spinner" aria-label="Loading"></sv-progress-ring>
<figcaption>Default</figcaption>
</figure>
<figure>
<sv-progress-ring class="comet-spinner" aria-label="Loading"></sv-progress-ring>
<figcaption>Comet</figcaption>
</figure>
<figure>
<sv-progress-ring class="pulse-spinner" aria-label="Loading"></sv-progress-ring>
<figcaption>Pulse</figcaption>
</figure>
<figure>
<sv-progress-ring class="dots-spinner" aria-label="Loading"></sv-progress-ring>
<figcaption>Dots</figcaption>
</figure>
<figure>
<sv-progress-ring class="dual-spinner" aria-label="Loading"></sv-progress-ring>
<figcaption>Dual</figcaption>
</figure>
<figure>
<sv-progress-ring class="material-spinner" aria-label="Loading"></sv-progress-ring>
<figcaption>Material</figcaption>
</figure>
</div>
<style>
116 collapsed lines
/*
--path-length is the circumference of the ring.
Each dash pattern adds up to one path length and wraps the circle once.
An orbit animation moves the offset by one path length per cycle.
*/
.spinner-gallery {
--path-length: calc(3.1415 * 100%);
display: flex;
flex-wrap: wrap;
gap: 2em;
justify-content: center;
padding-block: 1em;
figure {
display: grid;
gap: 0.75rem;
justify-items: center;
margin: 0;
}
figcaption {
font-size: 0.8rem;
color: var(--sv-gray-3);
}
sv-progress-ring {
inline-size: 3rem;
}
.comet-spinner:state(--indeterminate) {
&::part(svg) {
animation: spinner-rotate 0.7s linear infinite;
}
&::part(fill) {
stroke-dasharray: calc(var(--path-length) * 0.26) calc(var(--path-length) * 0.74);
animation: none;
}
}
.pulse-spinner:state(--indeterminate)::part(fill) {
stroke-dasharray: calc(var(--path-length) * 0.33) calc(var(--path-length) * 0.67);
animation:
spinner-orbit 1.5s linear infinite,
pulse-fade 1s ease-in-out infinite alternate;
}
.dots-spinner:state(--indeterminate) {
&::part(svg) {
animation: spinner-rotate 3s linear infinite;
}
&::part(fill) {
stroke-dasharray: 0.1px calc(var(--path-length) / 6 - 0.1px);
animation: none;
}
}
.dual-spinner:state(--indeterminate) {
&::part(svg) {
animation: spinner-rotate 1.2s linear infinite;
}
&::part(fill) {
stroke-dasharray: calc(var(--path-length) / 4) calc(var(--path-length) / 4);
animation: none;
}
}
.material-spinner:state(--indeterminate) {
&::part(svg) {
animation: spinner-rotate 1.8s linear infinite;
}
&::part(fill) {
animation: spinner-arc 1.4s ease-in-out infinite;
}
}
}
@keyframes spinner-rotate {
to {
rotate: 1turn;
}
}
@keyframes spinner-orbit {
to {
stroke-dashoffset: calc(-1 * var(--path-length));
}
}
@keyframes pulse-fade {
to {
opacity: 0.25;
}
}
@keyframes spinner-arc {
0% {
stroke-dasharray: calc(var(--path-length) * 0.05) calc(var(--path-length) * 0.95);
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: calc(var(--path-length) * 0.74) calc(var(--path-length) * 0.26);
stroke-dashoffset: calc(var(--path-length) * -0.24);
}
100% {
stroke-dasharray: calc(var(--path-length) * 0.05) calc(var(--path-length) * 0.95);
stroke-dashoffset: calc(-1 * var(--path-length));
}
}
</style>