Loaders & patterns
Flipping loader
Pure CSSThe classic square loader: half a turn on X, then half a turn on Y.
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);
}
}