Twisting puzzle cube
Pure CSSA 3×3×3 cube built as three layers that take turns twisting a quarter turn. Each layer is one flat box with the stickers painted on by gradients.
Sixteen cubes seen isometrically, rising and falling in a wave that travels along the diagonals. Each cube is one element: its top plus two folded pseudo-element sides.
Your edited version
rotateZ(-45deg), then tip it back with rotateX. "Up" is now the grid's own Z axis, so translateZ lifts a cube straight off the floor.::before / ::after are the sides, hinged on its edges and folded down.--d is row + column, so cubes on the same diagonal share a delay and the wave travels corner to corner.transform animates, so 16 cubes stay cheap.rotateX + rotateZ isometric flooronly the two visible sides, as ::before / ::aftertranslateZ along the floor normaldelay = (row + column) × step<div class="scene">
<div class="field">
<i style="--d:0"></i>
<i style="--d:1"></i>
<i style="--d:2"></i>
<i style="--d:3"></i>
<i style="--d:1"></i>
<i style="--d:2"></i>
<i style="--d:3"></i>
<i style="--d:4"></i>
<i style="--d:2"></i>
<i style="--d:3"></i>
<i style="--d:4"></i>
<i style="--d:5"></i>
<i style="--d:3"></i>
<i style="--d:4"></i>
<i style="--d:5"></i>
<i style="--d:6"></i>
</div>
</div>
.scene {
perspective: 800px;
}
.field {
display: grid;
grid-template-columns: repeat(4, 36px);
gap: 10px;
transform-style: preserve-3d;
/* isometric floor: turn 45deg, then tip back */
transform: translateY(18px) rotateX(58deg) rotateZ(-45deg);
}
/* the element itself is the top of the cube */
.field i {
--hue: calc(253 - var(--d) * 13); /* violet corner to teal corner */
position: relative;
width: 36px;
height: 36px;
background: hsl(var(--hue) 90% 80%);
transform-style: preserve-3d;
animation: wave 2.6s ease-in-out infinite;
animation-delay: calc(var(--d) * -0.28s);
}
.field i::before,
.field i::after {
content: '';
position: absolute;
}
/* side hinged on the bottom edge, folded down */
.field i::before {
left: 0;
top: 100%;
width: 100%;
height: 36px;
background: hsl(var(--hue) 85% 64%);
transform-origin: top;
transform: rotateX(-90deg);
}
/* side hinged on the left edge, folded down */
.field i::after {
right: 100%;
top: 0;
width: 36px;
height: 100%;
background: hsl(var(--hue) 55% 36%);
transform-origin: right;
transform: rotateY(-90deg);
}
@keyframes wave {
0%, 100% { transform: translateZ(0); }
50% { transform: translateZ(44px); }
}
@use 'sass:math';
$n: 4;
$cell: 28px;
$gap: 8px;
$lift: 34px;
.d-cubegrid {
display: grid;
grid-template-columns: repeat($n, $cell);
gap: $gap;
transform-style: preserve-3d;
// isometric view: turn the floor 45deg, then tip it back. "Up" is now the floor's own Z axis.
transform: translateY(14px) rotateX(58deg) rotateZ(-45deg);
// The <i> itself is the cube's top. The two sides that can ever be seen from this angle are
// pseudo-elements hinged on its edges and folded down, so a whole cube is a single element.
i {
--c: color-mix(in srgb, var(--accent-2) var(--mix), var(--accent));
position: relative;
width: $cell;
height: $cell;
background: color-mix(in srgb, var(--c) 78%, white);
transform-style: preserve-3d;
// translateZ only: the cube never leaves its grid cell, it just travels along the floor's normal
animation: d-cubegrid-wave 2.6s ease-in-out infinite;
animation-delay: calc(var(--d) * -0.28s);
&::before,
&::after {
content: '';
position: absolute;
}
// side facing down-right on screen (hinged on the bottom edge)
&::before {
left: 0;
top: 100%;
width: 100%;
height: $cell;
background: var(--c);
transform-origin: top;
transform: rotateX(-90deg);
}
// side facing down-left on screen (hinged on the left edge)
&::after {
right: 100%;
top: 0;
width: $cell;
height: 100%;
background: color-mix(in srgb, var(--c) 58%, black);
transform-origin: right;
transform: rotateY(-90deg);
}
}
// --d = column + row, so cubes on the same diagonal move together and the wave travels
@for $y from 0 to $n {
@for $x from 0 to $n {
i:nth-child(#{$y * $n + $x + 1}) {
--d: #{$x + $y};
--mix: #{math.round(math.div($x + $y, ($n - 1) * 2) * 100) * 1%};
}
}
}
}
@keyframes d-cubegrid-wave {
0%,
100% {
transform: translateZ(0);
}
50% {
transform: translateZ($lift);
}
}