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.
A chunky switch whose knob is a real cube. Checked, it tips over its right-hand edge onto the other half of the track, bringing the moon face up, and the track changes colour.
Your edited version
--u, tied to the canvas, so the track, the cube on it and the label under it are the same share of a gallery card, the editor and a recording canvas. The numbers below are those units.rotateX(58deg)). Its thickness is five copies of the pill stacked 2.4 units apart downward, lighter at the top: stacked layers are the cheap way to extrude a rounded shape.transform-origin is its bottom right edge: 100% 50% -17 units (the third value is Z, from the cube’s centre).rotateY(90deg) about that edge: exactly how a cube tips over. The centre swings up on an arc and lands one side further on, with no translate and no keyframes. The track is two cube widths long, so one tip is the whole travel.transform and opacity animate: the track’s “on” colour is a layer that fades in, and Off/On are two words in one grid cell. The whole thing is one <label> around a real role="switch" checkbox.transform-origin on the cube’s bottom edgeone rotateY(90deg) = a real rollstacked layers for the track’s depthcolour swap by cross-fading opacity<label class="toggle">
<input type="checkbox" role="switch" />
<span class="track" aria-hidden="true">
<i></i><i></i><i></i><i></i><i></i>
<span class="top"></span>
<i class="shadow"></i>
<span class="knob">
<span class="cube"><i><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.9 4.9 1.4 1.4"/><path d="m17.7 17.7 1.4 1.4"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.3 17.7-1.4 1.4"/><path d="m19.1 4.9-1.4 1.4"/></svg></i><i></i><i></i><i><svg viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round" aria-hidden="true"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg></i><i></i><i></i></span>
</span>
</span>
<span class="text">Dark mode<small aria-hidden="true"><span>Off</span><span>On</span></small></span>
</label>
/* one label: the static hit target around a real checkbox */
.toggle {
/* one base unit: every length below is a multiple of it, so the toggle is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.55vmin;
/* the label stands under the track, not beside it: side by side the pair is two and a half
times wider than tall, and at the 92vmin width limit the whole model falls under the floor */
display: grid;
justify-items: center;
gap: calc(18 * var(--u));
/* more room above than below: the cube stands up out of the track, so this is what centres
the drawn stack, cube, track and caption together, rather than the label's box */
padding: calc(25 * var(--u)) calc(10 * var(--u)) calc(11 * var(--u));
/* no colour of its own: the label is written on the stage, so it takes the stage's ink */
font-size: calc(15 * var(--u));
font-weight: 800;
cursor: pointer;
perspective: calc(800 * var(--u));
transform-style: preserve-3d;
user-select: none;
}
.toggle * {
pointer-events: none;
}
.toggle input {
position: absolute;
width: 1px;
height: 1px;
margin: 0;
opacity: 0;
}
/* 34-unit cube + 8 units of padding: two cube widths long, so one roll is the whole travel */
.track {
position: relative;
flex: none;
width: calc(84 * var(--u));
height: calc(50 * var(--u));
transform-style: preserve-3d;
transform: rotateX(58deg); /* lying on the floor, seen from above */
}
/* the thickness: copies of the pill stacked downward, darker as they go */
.track > i:not(.shadow) {
position: absolute;
inset: 0;
border-radius: calc(25 * var(--u));
}
.track > i:nth-child(1) { background: color-mix(in srgb, #eceefb 15%, #101326); transform: translateZ(calc(-12 * var(--u))); }
.track > i:nth-child(2) { background: color-mix(in srgb, #eceefb 18%, #101326); transform: translateZ(calc(-9.6 * var(--u))); }
.track > i:nth-child(3) { background: color-mix(in srgb, #eceefb 21%, #101326); transform: translateZ(calc(-7.2 * var(--u))); }
.track > i:nth-child(4) { background: color-mix(in srgb, #eceefb 24%, #101326); transform: translateZ(calc(-4.8 * var(--u))); }
.track > i:nth-child(5) { background: color-mix(in srgb, #eceefb 27%, #101326); transform: translateZ(calc(-2.4 * var(--u))); }
/* the top: a groove. "On" is a second layer that only fades in */
.top {
position: absolute;
inset: 0;
border-radius: calc(25 * var(--u));
background: color-mix(in srgb, #949bc0 30%, #141830);
box-shadow: inset 0 0 0 calc(1.5 * var(--u)) color-mix(in srgb, #eceefb 30%, #141830), inset 0 calc(3 * var(--u)) calc(8 * var(--u)) rgb(0 0 0 / 0.35);
}
.top::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(90deg, #8b6cff, #ff4d9d);
box-shadow: inset 0 0 0 calc(1.5 * var(--u)) color-mix(in srgb, #8b6cff, #fff 35%), inset 0 calc(3 * var(--u)) calc(8 * var(--u)) rgb(0 0 0 / 0.35);
opacity: 0;
transition: opacity 0.4s 0.1s;
}
.toggle input:checked ~ .track .top::before {
opacity: 1;
}
.toggle input:focus-visible ~ .track .top {
outline: calc(2 * var(--u)) solid #2ee6d6;
outline-offset: calc(4 * var(--u));
}
/* the cube's shadow, sliding along with it */
.shadow {
position: absolute;
top: calc(8 * var(--u));
left: calc(8 * var(--u));
width: calc(34 * var(--u));
height: calc(34 * var(--u));
border-radius: 50%;
background: radial-gradient(closest-side, rgb(0 0 0 / 0.5), transparent);
transform: translateZ(calc(0.5 * var(--u))) scale(1.25);
transition: transform 0.5s cubic-bezier(0.55, 0, 0.35, 1);
}
.toggle input:checked ~ .track .shadow {
transform: translateZ(calc(0.5 * var(--u))) translateX(calc(34 * var(--u))) scale(1.25);
}
/* lifted by half a side, so the cube stands on the track */
.knob {
position: absolute;
top: calc(8 * var(--u));
left: calc(8 * var(--u));
width: calc(34 * var(--u));
height: calc(34 * var(--u));
transform-style: preserve-3d;
transform: translateZ(calc(17 * var(--u)));
}
/* the pivot is the cube's bottom right edge: x = 100%, z = -17 units from its centre */
.cube {
--s: calc(34 * var(--u));
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform-origin: 100% 50% calc(-17 * var(--u));
transition: transform 0.5s cubic-bezier(0.55, 0, 0.35, 1);
}
/* checked: a quarter turn about that edge, a real roll */
.toggle input:checked ~ .track .cube {
transform: rotateY(90deg);
}
/* in the track's own space +z is up: face 1 is the top (sun), 4 the left (moon) */
/* 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)); }
.cube > i {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border-radius: calc(5 * var(--u));
background: linear-gradient(145deg, #fff, #dcd9ef);
transform-style: preserve-3d;
}
/* rounded faces leave holes at the corners: a square plate behind each one plugs them */
.cube > i::before {
content: '';
position: absolute;
inset: 0; /* full size: a smaller plate leaves a channel along each edge you can see into */
background: #c9c5e2;
transform: translateZ(calc(-3 * var(--u)));
}
.cube > i:nth-child(2) { background: linear-gradient(145deg, #eceaf8, #cbc7e3); }
.cube > i:nth-child(6) { background: linear-gradient(#d9d5ee, #b9b4d6); }
/* the sun and the moon inside a rim of the cube's own colour (like a keycap): at a rounded
corner a sliver of the side face shows, and a dark moon there looked like a hole */
.cube > i:nth-child(1) {
background: radial-gradient(circle, #fff6df, #ffe3a6);
box-shadow: inset 0 0 0 calc(2.5 * var(--u)) #dcd9ef;
color: #e08a00;
}
.cube > i:nth-child(4) {
background: radial-gradient(circle at 40% 35%, #34396e, #181b3c);
box-shadow: inset 0 0 0 calc(2.5 * var(--u)) #d4d0ea;
color: #e9e4ff;
}
.cube svg {
width: calc(22 * var(--u));
height: calc(22 * var(--u));
}
.cube > i:nth-child(4) svg {
width: calc(19 * var(--u));
height: calc(19 * var(--u));
}
/* the label and its state: two words in one grid cell, cross-faded */
.text {
display: grid;
justify-items: center;
gap: calc(2 * var(--u));
line-height: 1.1;
}
.text small {
display: grid;
font-size: calc(11 * var(--u));
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.text small span {
grid-area: 1 / 1;
transition: opacity 0.3s;
}
/* Off: the stage's ink, softened; On: violet leaning towards that ink, so both read on a dark
and on a light stage */
.text small span:first-child {
color: color-mix(in srgb, currentColor 65%, transparent);
}
.text small span + span {
color: color-mix(in srgb, #8b6cff 60%, currentColor);
opacity: 0;
}
.toggle input:checked ~ .text span:first-child { opacity: 0; }
.toggle input:checked ~ .text span + span { opacity: 1; }