The Material 3 expressive indicator: the filled part is a squiggle that keeps flowing, the rest is a flat track ending in a stop dot. The wave is one SVG path sliding by a wavelength.
Also known as: squiggly progress bar, material 3 wavy progress indicator, wave line loading bar.
<div class="ldr-wavy-progress-bar" role="progressbar" aria-label="Loading"> <span class="ldr-wavy-progress-bar-fill"><svg viewBox="0 0 200 12" aria-hidden="true"><path d="M0 6q5-5 10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0t10 0"></path></svg></span> </div>
.ldr-wavy-progress-bar {
--s: var(--ldr-size, 48px);
--c: var(--ldr-color, #3b82f6);
--w: calc(var(--s) * 6);
--g: calc(var(--s) / 6);
position: relative;
width: var(--w);
height: calc(var(--s) * 0.4);
}
.ldr-wavy-progress-bar::before {
content: "";
position: absolute;
top: 50%;
left: 0;
right: 0;
height: calc(var(--s) / 12);
margin-top: calc(var(--s) / -24);
border-radius: var(--s);
background: color-mix(in srgb, var(--ldr-color, #3b82f6) 22%, transparent);
animation: ldr-wavy-progress-bar-track 4s infinite;
}
.ldr-wavy-progress-bar::after {
content: "";
position: absolute;
top: 50%;
right: 0;
width: calc(var(--s) / 10);
height: calc(var(--s) / 10);
margin-top: calc(var(--s) / -20);
border-radius: 50%;
background: var(--c);
}
.ldr-wavy-progress-bar-fill {
position: absolute;
inset: 0;
overflow: hidden;
animation: ldr-wavy-progress-bar-fill 4s infinite;
}
.ldr-wavy-progress-bar svg {
display: block;
width: calc(var(--w) * 10 / 9);
height: 100%;
fill: none;
stroke: var(--c);
stroke-width: 2.5;
stroke-linecap: round;
animation: ldr-wavy-progress-bar-flow 0.9s linear infinite;
}
@keyframes ldr-wavy-progress-bar-flow {
to { transform: translateX(calc(var(--w) / -9)); }
}
@keyframes ldr-wavy-progress-bar-fill {
0% { clip-path: inset(0 100% 0 0); animation-timing-function: cubic-bezier(0.3, 0, 0.2, 1); }
20% { clip-path: inset(0 76% 0 0); animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1); }
32% { clip-path: inset(0 70% 0 0); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
55% { clip-path: inset(0 36% 0 0); animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1); }
66% { clip-path: inset(0 30% 0 0); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
84%, 94% { clip-path: inset(0 0 0 0); opacity: 1; }
100% { clip-path: inset(0 0 0 0); opacity: 0; }
}
@keyframes ldr-wavy-progress-bar-track {
0% { clip-path: inset(0 0 0 var(--g)); animation-timing-function: cubic-bezier(0.3, 0, 0.2, 1); }
20% { clip-path: inset(0 0 0 calc(24% + var(--g))); animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1); }
32% { clip-path: inset(0 0 0 calc(30% + var(--g))); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
55% { clip-path: inset(0 0 0 calc(64% + var(--g))); animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1); }
66% { clip-path: inset(0 0 0 calc(70% + var(--g))); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
84%, 100% { clip-path: inset(0 0 0 100%); }
}
@media (prefers-reduced-motion: reduce) {
.ldr-wavy-progress-bar, .ldr-wavy-progress-bar *, .ldr-wavy-progress-bar::before, .ldr-wavy-progress-bar::after, .ldr-wavy-progress-bar *::before, .ldr-wavy-progress-bar *::after {
animation-duration: 3s;
}
}--ldr-color on the element or a parent.
Currently previewing #3b82f6.--ldr-size. Every dimension in the snippet is
derived from it. Currently previewing 64px.animation shorthand.
Delays on the children are relative, so they keep their rhythm.role="status" and an
aria-label so screen readers announce the wait. The reduced-motion rule
at the end slows the animation for users who asked for less motion.