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.
Eight blocks pop into the air and drop one by one onto an isometric floor until they form a 2×2×2 cube, then the cube comes apart column by column and floats away, and it all starts again.
Your edited version
rotateX(58deg) rotateZ(45deg) turns a flat 2×2 grid into an isometric floor; from then on "up" is simply translateZ. A block only needs the three faces this camera can ever see: the top, the +x side and the +y side.animation-delay from --i (block 0…7) and --c (column 0…3), so eight blocks need only one "arrive" and one "leave" animation.translateZ … scale3d), and scale3d(0, 0, 0) rather than scale(0), because a 2D scale would leave a block's height standing as a line.rotateX(60deg) rotateZ(45deg) = isometric floorblock = 3 visible faces onlytwo nested animations: block drops, column leavesshared keyframes + delays from --i<div class="scene">
<div class="stack" role="img" aria-label="Loading">
<div class="col" style="--c:0">
<div class="block" style="--i:0; --l:0"><i></i><i></i><i></i></div>
<div class="block" style="--i:4; --l:1"><i></i><i></i><i></i></div>
</div>
<div class="col" style="--c:1">
<div class="block" style="--i:1; --l:0"><i></i><i></i><i></i></div>
<div class="block" style="--i:5; --l:1"><i></i><i></i><i></i></div>
</div>
<div class="col" style="--c:2">
<div class="block" style="--i:2; --l:0"><i></i><i></i><i></i></div>
<div class="block" style="--i:6; --l:1"><i></i><i></i><i></i></div>
</div>
<div class="col" style="--c:3">
<div class="block" style="--i:3; --l:0"><i></i><i></i><i></i></div>
<div class="block" style="--i:7; --l:1"><i></i><i></i><i></i></div>
</div>
</div>
</div>
.scene {
perspective: 800px;
}
/* the floor is the stack's own plane, turned isometric; "up" is translateZ */
.stack {
position: relative;
display: grid;
grid-template-columns: repeat(2, 38px);
grid-auto-rows: 38px;
transform-style: preserve-3d;
transform: rotateX(58deg) rotateZ(45deg);
}
/* the floor plate with the four landing places */
.stack::before {
content: '';
position: absolute;
inset: -8px;
border: 1px dashed rgb(140 150 220 / 0.34);
border-radius: 10px;
background:
linear-gradient(90deg, transparent calc(50% - 1px), rgb(140 150 220 / 0.16) 0 calc(50% + 1px), transparent 0),
linear-gradient(0deg, transparent calc(50% - 1px), rgb(140 150 220 / 0.16) 0 calc(50% + 1px), transparent 0),
rgb(139 108 255 / 0.1);
transform: translateZ(-1px);
}
/* a column: lifts away once the cube is complete, one after the other */
.col {
position: relative;
margin: 2px;
transform-style: preserve-3d;
transform-origin: 50% 50% 34px; /* the middle of the two-block column */
animation: stack-leave 5s linear infinite;
animation-delay: calc(var(--c) * 0.275s - 1.6s);
}
/* a block: pops up in the air, falls, bounces, sits; bottom layer first */
.block {
--z: calc(var(--l) * 34px); /* resting height */
--color: #8b6cff;
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform-origin: 50% 50% 17px;
animation: stack-arrive 5s linear infinite;
/* minus one loop: every block is already mid-cycle on load */
animation-delay: calc(var(--i) * 0.275s - 5s);
}
.block:nth-child(2) { --color: #2ee6d6; }
/* only the three faces we can ever see */
.block i {
position: absolute;
inset: 0;
border-radius: 2px;
}
.block i:nth-child(1) { /* top */
background: color-mix(in srgb, var(--color) 55%, #fff);
transform: translateZ(34px);
}
.block i:nth-child(2) { /* +x side, stood up on its right edge */
background: color-mix(in srgb, var(--color) 65%, #000);
transform-origin: 100% 50%;
transform: rotateY(90deg);
}
.block i:nth-child(3) { /* +y side, stood up on its bottom edge */
background: var(--color);
transform-origin: 50% 100%;
transform: rotateX(-90deg);
}
@keyframes stack-arrive {
0% {
transform: translateZ(calc(var(--z) + 56px)) scale3d(0, 0, 0);
animation-timing-function: ease-out;
}
3% {
transform: translateZ(calc(var(--z) + 56px)) scale3d(1, 1, 1);
animation-timing-function: ease-in; /* gravity */
}
10% {
transform: translateZ(var(--z)) scale3d(1, 1, 1);
animation-timing-function: ease-out;
}
12.5% {
transform: translateZ(calc(var(--z) + 5px)) scale3d(1, 1, 1);
animation-timing-function: ease-in;
}
15%, 76.5% {
transform: translateZ(var(--z)) scale3d(1, 1, 1);
}
/* its column is gone by now: reset out of sight */
76.6%, 100% {
transform: translateZ(calc(var(--z) + 56px)) scale3d(0, 0, 0);
}
}
@keyframes stack-leave {
0% {
transform: translateZ(0) rotateZ(0deg) scale3d(1, 1, 1);
animation-timing-function: ease-in;
}
7%, 31.25% {
transform: translateZ(72px) rotateZ(90deg) scale3d(0, 0, 0);
}
/* both its blocks have reset by now: come back, still empty */
31.35%, 100% {
transform: translateZ(0) rotateZ(0deg) scale3d(1, 1, 1);
}
}
$s: 34px; // block size
$cell: 38px; // block + gap
$time: 5s;
$step: 0.275s; // between one block and the next (5.5% of the loop)
$drop: 56px; // how high a block appears before it falls
// The floor is the root's own plane, turned into an isometric view; "up" is translateZ.
// Four columns stand on it (a 2×2 grid), each holding a bottom and a top block.
.d-blockstack {
position: relative;
display: grid;
grid-template-columns: repeat(2, $cell);
grid-auto-rows: $cell;
transform-style: preserve-3d;
transform: rotateX(58deg) rotateZ(45deg);
// the floor plate, with the four places the blocks will land
&::before {
content: '';
position: absolute;
inset: -8px;
border: 1px dashed var(--border-strong);
border-radius: 10px;
background:
linear-gradient(90deg, transparent calc(50% - 1px), var(--border) 0 calc(50% + 1px), transparent 0),
linear-gradient(0deg, transparent calc(50% - 1px), var(--border) 0 calc(50% + 1px), transparent 0),
color-mix(in srgb, var(--accent) 10%, transparent);
transform: translateZ(-1px);
}
// A column carries the "leave" animation: once the cube is complete, the columns lift away one
// after the other (--c = 0…3), then come back invisibly, empty, before their blocks drop again.
&__col {
position: relative;
margin: ($cell - $s) * 0.5;
transform-style: preserve-3d;
transform-origin: 50% 50% $s; // the middle of the two-block column
animation: d-blockstack-leave $time linear infinite;
animation-delay: calc(var(--c) * #{$step} - #{$time * 0.32});
}
// A block carries the "arrive" animation: pop into the air, fall, bounce, sit, then vanish once
// its column has flown off. --i = 0…7 staggers it (bottom layer first, back corner first).
// Delays are shifted back by a whole loop so every block is already mid-cycle on load.
&__block {
--z: calc(var(--l) * #{$s}); // resting height: bottom layer 0, top layer one block up
--c1: var(--accent);
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform-origin: 50% 50% ($s * 0.5);
animation: d-blockstack-arrive $time linear infinite;
animation-delay: calc(var(--i) * #{$step} - #{$time});
&:nth-child(2) {
--c1: var(--accent-2);
}
// only the three faces we can ever see: top, +x and +y
i {
position: absolute;
inset: 0;
border-radius: 2px;
}
i:nth-child(1) {
background: color-mix(in srgb, var(--c1) 55%, #fff);
transform: translateZ($s);
}
i:nth-child(2) {
background: color-mix(in srgb, var(--c1) 65%, #000);
transform-origin: 100% 50%;
transform: rotateY(90deg);
}
i:nth-child(3) {
background: var(--c1);
transform-origin: 50% 100%;
transform: rotateX(-90deg);
}
}
}
// Every stop keeps the same function list (translateZ, scale3d) so each value interpolates alone.
@keyframes d-blockstack-arrive {
0% {
transform: translateZ(calc(var(--z) + #{$drop})) scale3d(0, 0, 0);
animation-timing-function: ease-out;
}
3% {
transform: translateZ(calc(var(--z) + #{$drop})) scale3d(1, 1, 1);
animation-timing-function: ease-in; // gravity
}
10% {
transform: translateZ(var(--z)) scale3d(1, 1, 1);
animation-timing-function: ease-out;
}
12.5% {
transform: translateZ(calc(var(--z) + 5px)) scale3d(1, 1, 1);
animation-timing-function: ease-in;
}
15%,
76.5% {
transform: translateZ(var(--z)) scale3d(1, 1, 1);
}
// its column is gone by now: reset out of sight
76.6%,
100% {
transform: translateZ(calc(var(--z) + #{$drop})) scale3d(0, 0, 0);
}
}
@keyframes d-blockstack-leave {
0% {
transform: translateZ(0) rotateZ(0deg) scale3d(1, 1, 1);
animation-timing-function: ease-in;
}
7%,
31.25% {
transform: translateZ(72px) rotateZ(90deg) scale3d(0, 0, 0);
}
// both its blocks have reset by now: come back, still empty
31.35%,
100% {
transform: translateZ(0) rotateZ(0deg) scale3d(1, 1, 1);
}
}