Flipping loader
Pure CSSThe classic square loader: half a turn on X, then half a turn on Y, and both again, so the square ends the loop exactly where it began.
Seven real cuboids bouncing like an audio meter. Walls are squashed with scaleY and each lid rides down by the same amount, so nothing but transforms ever changes.
Your edited version
transform ever changes.scaleY() from a transform-origin: bottom center, so growing or shrinking a bar never touches layout — it is one compositor-only transform.translateY down by exactly the height the walls lost (var(--h) * (1 - level)), so it always stays glued to the top of the shrinking walls instead of floating.--u, tied to the canvas, so the meter is the same share of a gallery card, the editor and a recording canvas. A bar’s full height is a plain number in the markup (--hn), which CSS multiplies by --u: one number per bar, and the unit still decides the size.scaleY walls + translateY lid (no layout)shared keyframe stops keep lid and walls gluedhue, delay and duration per bar by :nth-childper-bar delay and duration<div class="scene">
<div class="equalizer" role="img" aria-label="Equalizer animation">
<div class="bar" style="--hn:70"><i></i><i></i><i></i></div>
<div class="bar" style="--hn:96"><i></i><i></i><i></i></div>
<div class="bar" style="--hn:118"><i></i><i></i><i></i></div>
<div class="bar" style="--hn:88"><i></i><i></i><i></i></div>
<div class="bar" style="--hn:108"><i></i><i></i><i></i></div>
<div class="bar" style="--hn:80"><i></i><i></i><i></i></div>
<div class="bar" style="--hn:60"><i></i><i></i><i></i></div>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the meter is the same share of a
gallery card, the editor, a full screen and a recording canvas */
--u: 0.41vmin;
perspective: calc(800 * var(--u));
}
.equalizer {
position: relative;
display: flex;
align-items: flex-end;
gap: calc(8 * var(--u));
height: calc(120 * var(--u));
transform-style: preserve-3d;
transform: translateY(calc(-11 * var(--u))) rotateX(-20deg) rotateY(-30deg);
}
/* glass floor: a strip centred on the bottom edge, laid flat */
.equalizer::before {
box-sizing: content-box; /* its size is its inside, its edge outside it, on any page */
content: '';
position: absolute;
right: calc(-12 * var(--u));
bottom: calc(-22 * var(--u));
left: calc(-12 * var(--u));
height: calc(44 * var(--u));
border-radius: calc(8 * var(--u));
transform: rotateX(90deg);
background: color-mix(in srgb, #8b6cff 12%, transparent);
border: calc(1 * var(--u)) solid color-mix(in srgb, #8b6cff 75%, transparent);
box-shadow: inset 0 0 calc(24 * var(--u)) color-mix(in srgb, #8b6cff 30%, transparent);
}
/* --hn is the bar's full height as a plain number (set in the markup). The box never changes
size: the walls are squashed with scaleY from the bottom, and the lid rides down with
translateY by exactly the amount the walls lost. Transforms only, zero layout. */
.bar {
--h: calc(var(--hn) * var(--u));
position: relative;
width: calc(18 * var(--u));
height: var(--h);
transform-style: preserve-3d;
}
/* violet → teal across the row, with its own phase and tempo so neighbours never move together */
.bar:nth-child(1) { --hue: 265; --delay: -0.4s; --dur: 2.7s; }
.bar:nth-child(2) { --hue: 250; --delay: -1.6s; --dur: 2.2s; }
.bar:nth-child(3) { --hue: 235; --delay: -0.9s; --dur: 3.1s; }
.bar:nth-child(4) { --hue: 220; --delay: -2.3s; --dur: 2.35s; }
.bar:nth-child(5) { --hue: 205; --delay: -0.2s; --dur: 2.9s; }
.bar:nth-child(6) { --hue: 190; --delay: -1.3s; --dur: 2.25s; }
.bar:nth-child(7) { --hue: 175; --delay: -2s; --dur: 2.6s; }
.bar i {
position: absolute;
top: 0;
left: 0;
width: calc(18 * var(--u));
height: 100%;
transform-origin: bottom center;
animation: var(--dur) ease-in-out var(--delay) infinite;
}
/* front wall */
.bar i:nth-child(1) {
background: linear-gradient(to top, hsl(var(--hue) 85% 45% / 0.92), hsl(var(--hue) 90% 68% / 0.92));
transform: translateZ(calc(9 * var(--u)));
animation-name: equalizer-front;
}
/* right wall, darker */
.bar i:nth-child(2) {
background: linear-gradient(to top, hsl(var(--hue) 75% 28% / 0.92), hsl(var(--hue) 80% 46% / 0.92));
transform: rotateY(90deg) translateZ(calc(9 * var(--u)));
animation-name: equalizer-side;
}
/* lid: an 18 × 18 square laid flat on top, lightest */
.bar i:nth-child(3) {
height: calc(18 * var(--u));
background: hsl(var(--hue) 95% 80%);
transform-origin: center;
transform: rotateX(90deg) translateZ(calc(9 * var(--u)));
animation-name: equalizer-lid;
}
/* the three animations share the same stops and easing, so walls and lid stay glued together */
@keyframes equalizer-front {
0% { transform: translateZ(calc(9 * var(--u))) scaleY(0.18); }
22% { transform: translateZ(calc(9 * var(--u))) scaleY(1); }
42% { transform: translateZ(calc(9 * var(--u))) scaleY(0.5); }
64% { transform: translateZ(calc(9 * var(--u))) scaleY(0.86); }
82% { transform: translateZ(calc(9 * var(--u))) scaleY(0.34); }
100% { transform: translateZ(calc(9 * var(--u))) scaleY(0.18); }
}
@keyframes equalizer-side {
0% { transform: rotateY(90deg) translateZ(calc(9 * var(--u))) scaleY(0.18); }
22% { transform: rotateY(90deg) translateZ(calc(9 * var(--u))) scaleY(1); }
42% { transform: rotateY(90deg) translateZ(calc(9 * var(--u))) scaleY(0.5); }
64% { transform: rotateY(90deg) translateZ(calc(9 * var(--u))) scaleY(0.86); }
82% { transform: rotateY(90deg) translateZ(calc(9 * var(--u))) scaleY(0.34); }
100% { transform: rotateY(90deg) translateZ(calc(9 * var(--u))) scaleY(0.18); }
}
/* walls lost (1 - level) of the height at each stop: the lid drops by the same distance */
@keyframes equalizer-lid {
0% { transform: translateY(calc(var(--h) * 0.82)) rotateX(90deg) translateZ(calc(9 * var(--u))); }
22% { transform: translateY(calc(var(--h) * 0)) rotateX(90deg) translateZ(calc(9 * var(--u))); }
42% { transform: translateY(calc(var(--h) * 0.5)) rotateX(90deg) translateZ(calc(9 * var(--u))); }
64% { transform: translateY(calc(var(--h) * 0.14)) rotateX(90deg) translateZ(calc(9 * var(--u))); }
82% { transform: translateY(calc(var(--h) * 0.66)) rotateX(90deg) translateZ(calc(9 * var(--u))); }
100% { transform: translateY(calc(var(--h) * 0.82)) rotateX(90deg) translateZ(calc(9 * var(--u))); }
}