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.
Click any tile: every tile flips, delayed by its distance from the one you clicked.
Your edited version
::before / ::after, backface-visibility: hidden) and flip when the grid gets data-flipped.transition-delay: calc(var(--d) * 70ms).--d, then toggles the attribute.pointer-events: none and the tiles auto. Both lie on the same 3D plane, and coplanar surfaces have no stable front-to-back order — without this the browser hit-tests the invisible grid in patches, and the cursor and clicks fail there.distance → --d → transition-delayone data attribute flips everythingtwo-sided tiles<div class="scene">
<div class="tiles"></div>
</div>
.scene {
perspective: 800px;
}
.tiles {
display: grid;
grid-template-columns: repeat(7, 42px);
gap: 5px;
transform-style: preserve-3d;
transform: rotateX(30deg);
/* the grid is on the same 3D plane as its tiles; coplanar surfaces have no stable order, so
in patches the browser would hit-test the grid instead of the tile (wrong cursor, dead clicks) */
pointer-events: none;
}
.tiles i {
position: relative;
height: 42px;
pointer-events: auto;
cursor: pointer;
transform-style: preserve-3d;
transition: transform 0.6s cubic-bezier(0.3, 1.3, 0.5, 1);
transition-delay: calc(var(--d, 0) * 70ms);
}
.tiles i::before,
.tiles i::after {
content: '';
position: absolute;
inset: 0;
border-radius: 7px;
backface-visibility: hidden;
}
.tiles i::before { background: #ffb547; }
.tiles i::after { background: #ff4d9d; transform: rotateY(180deg); }
.tiles[data-flipped] i {
transform: rotateY(180deg);
}
const grid = document.querySelector('.tiles');
const COLS = 7, ROWS = 5;
for (let i = 0; i < COLS * ROWS; i++) grid.append(document.createElement('i'));
const tiles = [...grid.children];
grid.addEventListener('click', (e) => {
const index = tiles.indexOf(e.target);
if (index < 0) return;
const r0 = Math.floor(index / COLS), c0 = index % COLS;
// 1) delays first…
tiles.forEach((tile, i) => {
const d = Math.hypot(Math.floor(i / COLS) - r0, (i % COLS) - c0);
tile.style.setProperty('--d', d.toFixed(2));
});
// 2) …then the change that triggers the transitions
grid.toggleAttribute('data-flipped');
});
.d-ripple {
display: grid;
grid-template-columns: repeat(7, 30px);
gap: 4px;
transform-style: preserve-3d;
transform: rotateX(30deg);
// The container lies on exactly the same 3D plane as its children. Coplanar surfaces have no
// stable front-to-back order, so in patches the browser hit-tests the (invisible) container
// instead of the child: wrong cursor, ignored hover. Only the children should catch the pointer.
pointer-events: none;
i {
position: relative;
pointer-events: auto;
height: 30px;
cursor: pointer;
transform-style: preserve-3d;
transition: transform 0.6s cubic-bezier(0.3, 1.3, 0.5, 1);
// --d = distance from the clicked tile, written by JS just before the flip
transition-delay: calc(var(--d, 0) * 70ms);
&::before,
&::after {
content: '';
position: absolute;
inset: 0;
border-radius: 6px;
backface-visibility: hidden;
}
&::before {
background: var(--warm);
}
&::after {
background: var(--hot);
transform: rotateY(180deg);
}
}
&[data-flipped] i {
transform: rotateY(180deg);
}
}