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.
Each checkbox is a small glass cube with the tick on its bottom face. :checked rolls the cube a quarter turn forward; the inputs are real, so keyboard and forms just work.
Your edited version
<input type="checkbox"> stays in the page — keyboard, forms and screen readers all keep working — it is only made invisible with opacity: 0. The <label> next to it is the static hit target, since the pressed element must never be the one that moves.perspective: 160px stage so all three read from the same angle.input:checked + label .cube adds one more rotateX(90deg) on top of the resting view angle — a quarter turn forward brings that bottom face round to the front.scaleX(0 → 1) from a fixed-width element instead of animating width, so it costs only a compositor transform.input:checked + labelcube rolls rotateX(90deg)static label = hit targetstrike-through with scaleX<ul class="check-list">
<li>
<input type="checkbox" id="check-0" checked />
<label for="check-0">
<span class="box">
<span class="cube"><i></i><i></i><i></i><i></i><i></i><i><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></i></span>
</span>
<span class="text">Set the perspective</span>
</label>
</li>
<li>
<input type="checkbox" id="check-1" />
<label for="check-1">
<span class="box">
<span class="cube"><i></i><i></i><i></i><i></i><i></i><i><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></i></span>
</span>
<span class="text">Preserve the 3D</span>
</label>
</li>
<li>
<input type="checkbox" id="check-2" />
<label for="check-2">
<span class="box">
<span class="cube"><i></i><i></i><i></i><i></i><i></i><i><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></i></span>
</span>
<span class="text">Flip the cube</span>
</label>
</li>
</ul>
.check-list {
display: grid;
gap: 7px;
width: 204px;
margin: 0;
padding: 0;
list-style: none;
}
.check-list li {
position: relative;
}
/* the real checkbox stays for keyboard/forms/screen readers, only made invisible */
.check-list input {
position: absolute;
inset: 0;
margin: 0;
opacity: 0;
pointer-events: none;
}
/* the label is the static hit target: nothing the pointer can touch ever moves */
.check-list label {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border: 1px solid #262b4a;
border-radius: 12px;
background: rgb(22 26 51 / 0.6);
color: #eceefb;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: border-color 0.2s;
}
.check-list label:hover {
border-color: #3a4070;
}
.check-list input:focus-visible + label {
outline: 2px solid #2ee6d6;
outline-offset: 2px;
}
/* a tiny stage of its own for every box, so all three cubes are seen from the same angle */
.box {
flex: none;
width: 22px;
height: 22px;
perspective: 160px;
pointer-events: none;
}
.cube {
position: relative;
display: block;
width: 22px;
height: 22px;
transform-style: preserve-3d;
/* view angle first, then the flip happens around the cube's own X axis */
transform: rotateX(-16deg) rotateY(-24deg) rotateX(0deg);
transition: transform 0.55s cubic-bezier(0.3, 1.5, 0.5, 1);
}
/* turn each face outward, then push it half a side out from the centre */
.cube i:nth-child(1) { transform: rotateY(0deg) translateZ(11px); }
.cube i:nth-child(2) { transform: rotateY(90deg) translateZ(11px); }
.cube i:nth-child(3) { transform: rotateY(180deg) translateZ(11px); }
.cube i:nth-child(4) { transform: rotateY(-90deg) translateZ(11px); }
.cube i:nth-child(5) { transform: rotateX(90deg) translateZ(11px); }
.cube i:nth-child(6) { transform: rotateX(-90deg) translateZ(11px); }
.cube i {
position: absolute;
inset: 0;
border-radius: 3px;
background: rgb(139 108 255 / 0.22);
border: 1px solid rgb(139 108 255 / 0.75);
box-shadow: inset 0 0 12px rgb(139 108 255 / 0.3);
}
/* the 6th face is the BOTTOM of the cube: it carries the tick and is out of sight at rest */
.cube i:last-child {
display: grid;
place-items: center;
border-color: #2ee6d6;
background: #2ee6d6;
box-shadow: 0 0 12px rgb(46 230 214 / 0.6);
color: #062b28;
backface-visibility: hidden; /* looking down into the glass cube you'd see the inside otherwise */
}
.cube i:last-child svg {
width: 15px;
height: 15px;
}
/* a quarter turn forward brings the bottom face to the front */
.check-list input:checked + label .cube {
transform: rotateX(-16deg) rotateY(-24deg) rotateX(90deg);
}
.text {
position: relative;
transition: opacity 0.3s;
}
/* the strike-through is a line scaled from 0 to 1: transform only, no layout, no repaint */
.text::after {
content: '';
position: absolute;
top: 52%;
right: -3px;
left: -3px;
height: 1.5px;
border-radius: 1px;
background: #2ee6d6;
transform: scaleX(0);
transform-origin: 0 50%;
transition: transform 0.35s ease-out;
}
.check-list input:checked + label .text {
opacity: 0.55;
}
.check-list input:checked + label .text::after {
transform: scaleX(1);
transition-delay: 0.15s;
}
@use '../mixins' as *;
$s: 22px;
.d-check {
display: grid;
gap: 7px;
width: 204px;
margin: 0;
padding: 0;
list-style: none;
li {
position: relative;
}
// The real checkbox stays in the page (keyboard, screen readers, forms); it is only invisible.
input {
position: absolute;
inset: 0;
margin: 0;
opacity: 0;
pointer-events: none;
}
// The label is the static hit target. Nothing the pointer can touch ever moves.
label {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 12px;
background: color-mix(in srgb, var(--surface-solid) 60%, transparent);
color: var(--text);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: border-color 0.2s;
&:hover {
border-color: var(--border-strong);
}
}
input:focus-visible + label {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
// A tiny stage of its own for every box, so all three are seen from the same angle.
&__box {
flex: none;
width: $s;
height: $s;
perspective: 160px;
pointer-events: none;
}
&__cube {
--s: #{$s};
position: relative;
display: block;
width: $s;
height: $s;
transform-style: preserve-3d;
// View angle first, then the flip: the flip happens around the cube's own X axis.
transform: rotateX(-16deg) rotateY(-24deg) rotateX(0deg);
transition: transform 0.55s cubic-bezier(0.3, 1.5, 0.5, 1);
@include cube-faces(var(--s));
i {
position: absolute;
inset: 0;
border-radius: 3px;
@include face(var(--accent), 22%);
}
// The 6th face is the BOTTOM of the cube: it carries the tick and is out of sight at rest.
i:last-child {
display: grid;
place-items: center;
border-color: var(--accent-2);
background: var(--accent-2);
box-shadow: 0 0 12px color-mix(in srgb, var(--accent-2) 60%, transparent);
color: #062b28;
backface-visibility: hidden; // looking down into the glass cube you would see its inside
svg {
width: 15px;
height: 15px;
}
}
}
// A quarter turn forward brings the bottom face to the front.
input:checked + label &__cube {
transform: rotateX(-16deg) rotateY(-24deg) rotateX(90deg);
}
// The strike-through is a line scaled from 0 to 1: transform only, no layout, no repaint.
&__text {
position: relative;
transition: opacity 0.3s;
&::after {
content: '';
position: absolute;
top: 52%;
right: -3px;
left: -3px;
height: 1.5px;
border-radius: 1px;
background: var(--accent-2);
transform: scaleX(0);
transform-origin: 0 50%;
transition: transform 0.35s ease-out;
}
}
input:checked + label &__text {
opacity: 0.55;
&::after {
transform: scaleX(1);
transition-delay: 0.15s;
}
}
}