3D equalizer
Pure CSSSeven 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.
The 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.
Your edited version
perspective() function goes inside the transform itself.perspective.--u, tied to the canvas — the perspective() distance too, 260 units — so the square and the depth of its flip are the same share of a gallery card, the editor and a recording canvas.perspective() inside transformmulti-step @keyframesno wrapper needed<div class="loader"></div>
.loader {
/* one base unit: every length below is a multiple of it, so the loader is the same share of a
gallery card, the editor, a full screen and a recording canvas */
--u: 0.46vmin;
width: calc(90 * var(--u));
height: calc(90 * var(--u));
border-radius: calc(12 * var(--u));
background: linear-gradient(135deg, #2ee6d6, #8b6cff);
box-shadow: 0 0 calc(30 * var(--u)) rgb(139 108 255 / 0.6);
/* each step is a 0.9s half turn, eased on its own */
animation: flip 3.6s ease-in-out infinite;
}
/* the perspective() function scales with the loader too, so the flip keeps its depth */
@keyframes flip {
0% { transform: perspective(calc(260 * var(--u))) rotateX(0deg) rotateY(0deg); }
25% { transform: perspective(calc(260 * var(--u))) rotateX(-180deg) rotateY(0deg); }
50% { transform: perspective(calc(260 * var(--u))) rotateX(-180deg) rotateY(-180deg); }
75% { transform: perspective(calc(260 * var(--u))) rotateX(-360deg) rotateY(-180deg); }
100% { transform: perspective(calc(260 * var(--u))) rotateX(-360deg) rotateY(-360deg); }
}