Magnetic button
CSS + JSInside its field the button shifts and leans toward the pointer while its label floats higher, so the two slide apart. JS only reports the pointer position; release it and a springy transition takes it home.
The tab panels are the four long sides of a prism. Radio inputs and labels pick a tab; each one sets a step count and the prism turns to that face.
Your edited version
~ general sibling combinator can reach both the nav and the prism from any of them.n gets rotateX(n * -90deg) translateZ(46 units).translateZ(-46 units)) so the panel currently in front sits exactly at z = 0 and its text stays sharp.n sets --step to n - 1 on the prism; rotateX(calc(var(--step) * 90deg)) turns to that face, and one shared transition animates every possible jump.backface-visibility: hidden on the panels stops the back faces from showing through as the prism turns.--u, tied to the canvas, text included, so the tabs are the same share of a gallery card, the editor and a recording canvas. The tabs are the model, so there is no separate control row: the whole thing fills the frame.radio:checked ~ sibling--step × 90degtranslateZ(-h / 2) keeps the front at z = 0backface-visibility: hidden<div class="tabs">
<input type="radio" name="tabs" id="tab-0" aria-label="Front" checked />
<input type="radio" name="tabs" id="tab-1" aria-label="Bottom" />
<input type="radio" name="tabs" id="tab-2" aria-label="Back" />
<input type="radio" name="tabs" id="tab-3" aria-label="Top" />
<div class="tabs-nav">
<label for="tab-0">Front</label>
<label for="tab-1">Bottom</label>
<label for="tab-2">Back</label>
<label for="tab-3">Top</label>
</div>
<div class="tabs-view">
<div class="tabs-prism">
<section style="--i:0;--c:#8b6cff"><b>Front</b><p>The face you start on. It sits at z = 0, so its text stays sharp.</p></section>
<section style="--i:1;--c:#2ee6d6"><b>Bottom</b><p>A quarter turn forward brings the bottom face up to the front.</p></section>
<section style="--i:2;--c:#ff4d9d"><b>Back</b><p>Half a turn. Placed with a half turn too, so it reads upright.</p></section>
<section style="--i:3;--c:#ffb547"><b>Top</b><p>Three quarter turns, all driven by one custom property.</p></section>
</div>
</div>
</div>
.tabs {
/* one base unit: every length below is a multiple of it, so the tabs are the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.42vmin;
position: relative;
display: grid;
gap: calc(20 * var(--u));
width: calc(204 * var(--u));
}
/* real radios, invisible but focusable; they come first so ~ can reach everything else */
.tabs input {
position: absolute;
top: 0;
left: 0;
width: calc(1 * var(--u));
height: calc(1 * var(--u));
margin: 0;
opacity: 0;
pointer-events: none;
}
.tabs-nav {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: calc(3 * var(--u));
padding: calc(3 * var(--u));
border: calc(1 * var(--u)) solid #262b4a;
border-radius: calc(10 * var(--u));
/* opaque, so the tab names read on either stage: see-through, on the light stage the bar was mid
grey and the names on it 1.8:1 */
background: #11142a;
}
.tabs-nav label {
padding: calc(5 * var(--u)) 0;
border-radius: calc(7 * var(--u));
color: #949bc0;
font-size: calc(11 * var(--u));
font-weight: 700;
text-align: center;
cursor: pointer;
transition: background 0.25s, color 0.25s;
}
.tabs-nav label:hover {
color: #eceefb;
}
/* the window the prism is seen through: static, and NOT clipped (clipping would flatten it) */
.tabs-view {
height: calc(92 * var(--u));
perspective: calc(520 * var(--u));
pointer-events: none;
}
/* pulled back by half its depth so the face in front sits exactly at z = 0 */
.tabs-prism {
position: relative;
height: 100%;
transform-style: preserve-3d;
transform: translateZ(calc(-46 * var(--u))) rotateX(calc(var(--step, 0) * 90deg));
transition: transform 0.75s cubic-bezier(0.3, 1.2, 0.4, 1);
}
/* face n: n quarter turns backward, then out by half the depth -> front, bottom, back, top */
.tabs-prism section {
position: absolute;
inset: 0;
display: grid;
align-content: center;
gap: calc(4 * var(--u));
padding: 0 calc(17 * var(--u));
/* an inner line plus a soft shade instead of a hard border: turning, a face passes
side-on, where a 1 units line breaks up; the shade survives */
box-shadow:
inset 0 0 0 calc(1 * var(--u)) color-mix(in srgb, var(--c) 70%, transparent),
inset 0 0 calc(8 * var(--u)) color-mix(in srgb, var(--c) 28%, transparent);
border-radius: calc(10 * var(--u));
background:
linear-gradient(140deg, color-mix(in srgb, var(--c) 34%, transparent), transparent 70%),
rgb(11 13 24); /* opaque: its words read the same on either stage */
backface-visibility: hidden;
transform: rotateX(calc(var(--i) * -90deg)) translateZ(calc(46 * var(--u)));
}
.tabs-prism section b {
color: var(--c);
font-size: calc(15 * var(--u));
font-weight: 800;
}
.tabs-prism section p {
margin: 0;
color: #949bc0;
font-size: calc(11.5 * var(--u));
line-height: 1.4;
}
/* one rule per tab: the nth radio turns the prism n-1 steps */
.tabs input:nth-of-type(1):checked ~ .tabs-view .tabs-prism { --step: 0; }
.tabs input:nth-of-type(2):checked ~ .tabs-view .tabs-prism { --step: 1; }
.tabs input:nth-of-type(3):checked ~ .tabs-view .tabs-prism { --step: 2; }
.tabs input:nth-of-type(4):checked ~ .tabs-view .tabs-prism { --step: 3; }
/* ...and lights the matching label */
.tabs input:nth-of-type(1):checked ~ .tabs-nav label:nth-of-type(1),
.tabs input:nth-of-type(2):checked ~ .tabs-nav label:nth-of-type(2),
.tabs input:nth-of-type(3):checked ~ .tabs-nav label:nth-of-type(3),
.tabs input:nth-of-type(4):checked ~ .tabs-nav label:nth-of-type(4) {
background: #6a45f5; /* deep enough for white words, 5.9:1: #8b6cff was 3.7:1 */
color: #fff;
}
/* and it answers the pointer: this selector far outweighs the block's label:hover, so the tab you
are already on was the one tab that did nothing. Darker again -- #5537c4 takes white further
past the 5.9:1 above, never back towards it. */
.tabs input:nth-of-type(1):checked ~ .tabs-nav label:nth-of-type(1):hover,
.tabs input:nth-of-type(2):checked ~ .tabs-nav label:nth-of-type(2):hover,
.tabs input:nth-of-type(3):checked ~ .tabs-nav label:nth-of-type(3):hover,
.tabs input:nth-of-type(4):checked ~ .tabs-nav label:nth-of-type(4):hover {
background: #5537c4;
}
.tabs input:nth-of-type(1):focus-visible ~ .tabs-nav label:nth-of-type(1),
.tabs input:nth-of-type(2):focus-visible ~ .tabs-nav label:nth-of-type(2),
.tabs input:nth-of-type(3):focus-visible ~ .tabs-nav label:nth-of-type(3),
.tabs input:nth-of-type(4):focus-visible ~ .tabs-nav label:nth-of-type(4) {
outline: calc(2 * var(--u)) solid #2ee6d6;
outline-offset: calc(2 * var(--u));
}