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.
Mechanical keycaps with sloped sides on a tilted plate. Hover or focus a key and its cap sinks into the plate with a glow; the button itself never moves, only the cap inside it.
Your edited version
rotateX(54deg) rotateZ(-30deg) turns it and lays it back, and preserve-3d lets everything on it rise up along its Z axis.--u, tied to the canvas — the plate, the keys, the slope of their sides — so the deck is the same share of a gallery card, the editor and a recording canvas. The numbers below are those units.clip-path trapezoid standing on an edge of the key, hinged there (transform-origin: 50% 100%) and tipped up by atan(height / inset) ≈ 65°, so its top edge lands exactly on the top face. Its length is √(height² + inset²).translateY(-22 units) rotate(90deg) translateY(22 units) moves the pivot to the middle, rotates, and moves it back.<button> is the hit target and never moves; only the .cap inside it (with pointer-events: none) sinks with translateZ(-7 units). An empty ::after lid where the top sits at rest makes the top clickable too.opacity: a teal pool on the plate and a wash over the legend. :focus-visible presses the key as well, so it works from the keyboard.trapezoid sides: clip-path + rotateX(atan(h / inset))rotate each side around the key centrestatic <button>, cap sinks with translateZglow faded with opacity<div class="scene">
<div class="deck">
<button type="button" class="key"><i class="glow"></i><span class="cap"><i></i><i></i><i></i><i></i><b>C</b></span></button>
<button type="button" class="key"><i class="glow"></i><span class="cap"><i></i><i></i><i></i><i></i><b>S</b></span></button>
<button type="button" class="key"><i class="glow"></i><span class="cap"><i></i><i></i><i></i><i></i><b>S</b></span></button>
<button type="button" class="key hot"><i class="glow"></i><span class="cap"><i></i><i></i><i></i><i></i><b>3D</b></span></button>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the deck is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.54vmin;
perspective: calc(800 * var(--u));
/* flat, not preserve-3d: the back of the tilted plate lies behind z = 0, and any box around the
scene that shares its 3D space (a page wrapper with preserve-3d) would sit in front of the
keys there and take their pointer. Flat, the deck is drawn into the scene's own plane, on top. */
transform-style: flat;
/* the plate is tilted, so parts of it lie behind z = 0: only the keys may take the pointer */
pointer-events: none;
}
/* a small keyboard plate lying on the floor, turned a little. Two by two, not one row of four:
a row laid back this far is three and a half times wider than it is tall, so at the 92vmin
width limit the deck stands on the 40vmin floor with nothing to spare */
.deck {
position: relative;
display: grid;
grid-template-columns: repeat(2, auto);
gap: calc(7 * var(--u));
padding: calc(9 * var(--u));
border-radius: calc(9 * var(--u));
background:
linear-gradient(160deg, rgb(255 255 255 / 0.07), transparent 60%),
color-mix(in srgb, #8b6cff 12%, #141830);
box-shadow: inset 0 0 0 calc(1 * var(--u)) rgb(140 150 220 / 0.34);
transform-style: preserve-3d;
transform: rotateX(54deg) rotateZ(-30deg); /* read right to left: turn it, then lay it back */
}
/* the plate's thickness: its front and left edges hang down from the top (-z) */
.deck::before,
.deck::after {
content: '';
position: absolute;
background: color-mix(in srgb, #8b6cff 22%, #0e1122);
}
.deck::before {
top: 100%;
right: calc(8 * var(--u));
left: calc(8 * var(--u));
height: calc(12 * var(--u));
transform-origin: 50% 0;
transform: rotateX(-90deg);
}
.deck::after {
top: calc(8 * var(--u));
right: 100%;
bottom: calc(8 * var(--u));
width: calc(12 * var(--u));
background: color-mix(in srgb, #8b6cff 18%, #0b0d1a);
transform-origin: 100% 50%;
transform: rotateY(90deg);
}
/* the button is the hit target and never moves */
.key {
--c: color-mix(in srgb, #8b6cff 38%, #141830);
--ink: #eceefb;
position: relative;
width: calc(44 * var(--u));
height: calc(44 * var(--u));
padding: 0;
border: 0;
border-radius: calc(6 * var(--u));
background: none;
color: var(--ink);
font: 800 calc(16 * var(--u))/1 Inter, system-ui, sans-serif;
cursor: pointer;
pointer-events: auto;
transform-style: preserve-3d;
}
.key.hot {
--c: #ff4d9d;
--ink: #fff;
}
/* an invisible lid where the cap's top is at rest, so pointing at the top hits the button too */
.key::after {
content: '';
position: absolute;
inset: calc(6 * var(--u));
transform: translateZ(calc(13 * var(--u)));
}
.key:focus-visible {
outline: calc(2 * var(--u)) solid #2ee6d6;
outline-offset: calc(3 * var(--u));
}
/* the cap: four sloped sides and a top, moved as one */
.cap {
position: absolute;
inset: 0;
pointer-events: none;
transform-style: preserve-3d;
transition: transform 0.16s cubic-bezier(0.3, 0.7, 0.4, 1);
}
/* a square plate just under the top: plugs the corners the rounded top leaves open */
.cap::before {
content: '';
position: absolute;
inset: calc(7 * var(--u));
background: color-mix(in srgb, var(--c), #000 30%);
transform: translateZ(calc(12 * var(--u)));
}
/* one sloped side: 13 units high, 6 units inset, so it is √(13² + 6²) = 14.32 units long,
stands on the key's front edge and tips up by atan(13 / 6) = 65.2deg */
.cap i {
position: absolute;
top: calc(29.68 * var(--u)); /* 44 - 14.32: its bottom edge is the key's front edge */
left: 0;
width: calc(44 * var(--u));
height: calc(14.32 * var(--u));
clip-path: polygon(0 100%, 100% 100%, calc(100% - 6 * var(--u)) 0, calc(6 * var(--u)) 0);
transform-origin: 50% 100%;
}
/* the same side turned around the key's centre (22 units above the hinge); shaded per side */
.cap i:nth-child(1) {
background: linear-gradient(color-mix(in srgb, var(--c), #000 20%), color-mix(in srgb, var(--c), #000 26%));
transform: translateY(calc(-22 * var(--u))) rotate(0deg) translateY(calc(22 * var(--u))) rotateX(-65.2deg);
}
.cap i:nth-child(2) {
background: linear-gradient(color-mix(in srgb, var(--c), #000 36%), color-mix(in srgb, var(--c), #000 42%));
transform: translateY(calc(-22 * var(--u))) rotate(90deg) translateY(calc(22 * var(--u))) rotateX(-65.2deg);
}
.cap i:nth-child(3) {
background: linear-gradient(color-mix(in srgb, var(--c), #000 2%), color-mix(in srgb, var(--c), #000 8%));
transform: translateY(calc(-22 * var(--u))) rotate(180deg) translateY(calc(22 * var(--u))) rotateX(-65.2deg);
}
.cap i:nth-child(4) {
background: linear-gradient(color-mix(in srgb, var(--c), #000 8%), color-mix(in srgb, var(--c), #000 14%));
transform: translateY(calc(-22 * var(--u))) rotate(270deg) translateY(calc(22 * var(--u))) rotateX(-65.2deg);
}
/* the top: slightly dished, with the legend */
.cap b {
position: absolute;
inset: calc(6 * var(--u));
display: grid;
place-items: center;
border-radius: calc(4 * var(--u));
background: radial-gradient(ellipse 80% 70% at 50% 45%, color-mix(in srgb, var(--c), #fff 10%), var(--c) 75%);
box-shadow: inset 0 0 0 calc(1 * var(--u)) color-mix(in srgb, var(--c), #fff 18%);
text-shadow: 0 calc(1 * var(--u)) 0 color-mix(in srgb, var(--c), #000 40%);
transform: translateZ(calc(13 * var(--u)));
}
/* the lit legend: a teal wash, faded in */
.cap b::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(circle, rgb(46 230 214 / 0.45), rgb(46 230 214 / 0.12));
opacity: 0;
transition: opacity 0.2s;
}
/* light spilling onto the plate around a pressed key */
.glow {
position: absolute;
inset: calc(-9 * var(--u));
border-radius: calc(14 * var(--u));
background: radial-gradient(closest-side, rgb(46 230 214 / 0.75), rgb(46 230 214 / 0.25) 70%, transparent);
opacity: 0;
pointer-events: none;
transform: translateZ(calc(0.5 * var(--u)));
transition: opacity 0.2s;
}
/* pressed: the cap sinks into the plate, the lights come on */
.key:is(:hover, :focus-visible) .cap {
transform: translateZ(calc(-7 * var(--u)));
}
.key:is(:hover, :focus-visible) .cap b::after,
.key:is(:hover, :focus-visible) .glow {
opacity: 1;
}
.key:active .cap {
transform: translateZ(calc(-10 * var(--u)));
}