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.
Interactive with no JavaScript: checked radios steer the cube through sibling selectors.
Your edited version
<input type="radio"> elements hold the state — the browser handles clicks and arrow keys.input:checked ~ .cube can reach it.transition animates between them.appearance: none lets you restyle the radios as buttons. Keep the aria-labels.:checked ~ sibling selectortransition on transformappearance: none<div class="picker">
<div class="controls-and-cube">
<input type="radio" name="face" aria-label="Front" checked>
<input type="radio" name="face" aria-label="Right">
<input type="radio" name="face" aria-label="Back">
<input type="radio" name="face" aria-label="Left">
<input type="radio" name="face" aria-label="Top">
<input type="radio" name="face" aria-label="Bottom">
<div class="cube">
<div>Front</div><div>Right</div><div>Back</div>
<div>Left</div><div>Top</div><div>Bottom</div>
</div>
</div>
</div>
.picker {
perspective: 800px;
}
.controls-and-cube {
display: grid;
grid-template-columns: repeat(6, auto);
justify-content: center;
gap: 40px 8px;
transform-style: preserve-3d;
}
input {
appearance: none;
width: 26px;
height: 26px;
margin: 0;
border: 2px solid #5a6188;
border-radius: 8px;
cursor: pointer;
}
input:checked {
background: #2ee6d6;
border-color: #2ee6d6;
box-shadow: 0 0 12px #2ee6d6;
}
.cube {
--s: 120px;
--view: rotateX(-18deg) rotateY(-22deg);
grid-row: 1;
grid-column: 1 / -1;
justify-self: center;
position: relative;
width: var(--s);
height: var(--s);
margin-top: 30px;
transform-style: preserve-3d;
transform: var(--view);
transition: transform 0.9s cubic-bezier(0.3, 1.3, 0.5, 1);
}
/* the whole trick: a checked radio restyles a LATER sibling */
input:nth-of-type(1):checked ~ .cube { transform: var(--view) rotateY(0deg); }
input:nth-of-type(2):checked ~ .cube { transform: var(--view) rotateY(-90deg); }
input:nth-of-type(3):checked ~ .cube { transform: var(--view) rotateY(-180deg); }
input:nth-of-type(4):checked ~ .cube { transform: var(--view) rotateY(90deg); }
input:nth-of-type(5):checked ~ .cube { transform: var(--view) rotateX(-90deg); }
input:nth-of-type(6):checked ~ .cube { transform: var(--view) rotateX(90deg); }
.cube > * {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font: 700 1rem system-ui;
background: rgb(46 230 214 / 0.25);
border: 1px solid rgb(46 230 214 / 0.8);
}
/* turn each face outward, then push it half a side from the centre */
.cube > :nth-child(1) { transform: rotateY(0deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(2) { transform: rotateY(90deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(3) { transform: rotateY(180deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(4) { transform: rotateY(-90deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(5) { transform: rotateX(90deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(6) { transform: rotateX(-90deg) translateZ(calc(var(--s) / 2)); }
@use 'sass:list';
@use '../mixins' as *;
// Rotation that brings each face to the front: Front, Right, Back, Left, Top, Bottom.
$views: (rotateY(0deg), rotateY(-90deg), rotateY(-180deg), rotateY(90deg), rotateX(-90deg), rotateX(90deg));
// The cube in the middle of the stage, the six squares in the control dock at the bottom (the
// same place as every demo's controls). They stay the cube's siblings: that is what lets
// `:checked ~ .d-radio__cube` steer it without JavaScript.
.d-radio {
display: grid;
grid-template-columns: repeat(6, auto);
grid-template-rows: minmax(0, 1fr) auto;
justify-content: center;
gap: 0 8px;
width: 100%;
height: 100%;
padding: 10px 14px 14px;
transform-style: preserve-3d;
input {
appearance: none;
width: 22px;
height: 22px;
margin: 0;
border: 2px solid var(--border-strong);
border-radius: 7px;
cursor: pointer;
transition: background 0.2s;
&:hover {
border-color: var(--accent-2);
}
&:checked {
background: var(--accent-2);
border-color: var(--accent-2);
box-shadow: 0 0 12px var(--accent-2);
}
}
&__cube {
--s: 92px;
grid-row: 1;
grid-column: 1 / -1;
place-self: center;
position: relative;
width: var(--s);
height: var(--s);
transform-style: preserve-3d;
transform: rotateX(-18deg) rotateY(-22deg);
transition: transform 0.9s cubic-bezier(0.3, 1.3, 0.5, 1);
@include cube-faces(var(--s));
i {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-size: 13px;
font-style: normal;
font-weight: 700;
@include face(var(--accent-2));
}
}
// The whole trick: a checked radio restyles a LATER sibling. No script involved.
@for $i from 1 through 6 {
input:nth-of-type(#{$i}):checked ~ &__cube {
transform: rotateX(-18deg) rotateY(-22deg) #{list.nth($views, $i)};
}
}
}