Two segmented LED columns jump between levels like a stereo VU meter. The segments are a repeating-gradient mask and the level is a stepped clip-path, so it moves in whole LEDs.
Also known as: led level meter, audio meter animation, stereo level loader.
<div class="ldr-vu-meter" role="status" aria-label="Loading"> <span></span><span></span> </div>
.ldr-vu-meter {
--s: var(--ldr-size, 48px);
--c: var(--ldr-color, #3b82f6);
display: flex;
gap: calc(var(--s) / 7);
height: var(--s);
}
.ldr-vu-meter span {
position: relative;
width: calc(var(--s) / 2.6);
-webkit-mask: repeating-linear-gradient(to top, #000 0 calc(var(--s) / 14), transparent 0 calc(var(--s) / 10));
mask: repeating-linear-gradient(to top, #000 0 calc(var(--s) / 14), transparent 0 calc(var(--s) / 10));
}
.ldr-vu-meter span::before,
.ldr-vu-meter span::after {
content: "";
position: absolute;
inset: 0;
background: color-mix(in srgb, var(--ldr-color, #3b82f6) 16%, transparent);
}
.ldr-vu-meter span::after {
background: linear-gradient(to top, color-mix(in srgb, var(--ldr-color, #3b82f6) 65%, transparent), var(--c) 70%);
animation: ldr-vu-meter-level 1.6s step-end infinite;
}
.ldr-vu-meter span:nth-child(2)::after {
animation-duration: 1.36s;
animation-delay: -0.5s;
}
@keyframes ldr-vu-meter-level {
0%, 100% { clip-path: inset(60% 0 0 0); }
8% { clip-path: inset(30% 0 0 0); }
16% { clip-path: inset(20% 0 0 0); }
24% { clip-path: inset(40% 0 0 0); }
32% { clip-path: inset(50% 0 0 0); }
40% { clip-path: inset(20% 0 0 0); }
48% { clip-path: inset(0% 0 0 0); }
56% { clip-path: inset(30% 0 0 0); }
64% { clip-path: inset(50% 0 0 0); }
72% { clip-path: inset(70% 0 0 0); }
80% { clip-path: inset(40% 0 0 0); }
88% { clip-path: inset(50% 0 0 0); }
}
@media (prefers-reduced-motion: reduce) {
.ldr-vu-meter, .ldr-vu-meter *, .ldr-vu-meter::before, .ldr-vu-meter::after, .ldr-vu-meter *::before, .ldr-vu-meter *::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.