Hover card fan
Pure CSSThree overlapping cards: the one you point at straightens and comes toward you while the others dim and lean away. Static strips catch the pointer, so nothing flickers.
Radio buttons open one section at a time: its panel swings down on its top edge like a flap while the headers below slide out of the way. No height is animated.
Your edited version
<input type="radio">s (visually hidden, not display: none) drive the state — that keeps arrow-key navigation and screen readers working for free.transform-origin: top center plus rotateX(-90deg) to fold it flat, edge-on to the viewer, until its radio is checked.height. The frame has a fixed size; sections below the open one are pushed down with translateY by exactly one panel's height instead.~ (general sibling) plus :checked lets one radio style both its own section and every section after it, all from CSS alone.:checked ~ sibling selectorsrotateX on transform-origin: toptranslateY instead of heightfixed-size frame<div class="scene">
<div class="accordion">
<input type="radio" name="acc" id="acc-0" checked />
<input type="radio" name="acc" id="acc-1" />
<input type="radio" name="acc" id="acc-2" />
<div class="sec" style="--i:0">
<label for="acc-0">Perspective</label>
<div class="panel">The camera distance. Put it on the parent; smaller values look more dramatic.</div>
</div>
<div class="sec" style="--i:1">
<label for="acc-1">Preserve-3d</label>
<div class="panel">Without it, children are flattened into the plane of their parent.</div>
</div>
<div class="sec" style="--i:2">
<label for="acc-2">Backface</label>
<div class="panel">Hide the reverse side of a face so text never shows mirrored.</div>
</div>
</div>
</div>
.scene {
perspective: 800px;
}
.accordion {
position: relative;
width: 196px;
height: 187px; /* 3 × (header 32px + gap 5px) + panel 71px */
transform-style: preserve-3d;
transform: rotateY(-18deg) rotateX(4deg);
}
/* real radios keep it keyboard-friendly (arrow keys move between sections); only hidden */
.accordion > input {
position: absolute;
width: 1px;
height: 1px;
margin: 0;
opacity: 0;
pointer-events: none;
}
.sec {
position: absolute;
top: calc(var(--i) * 37px);
right: 0;
left: 0;
height: 32px;
transform-style: preserve-3d;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.accordion label {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 12px;
border: 1px solid rgb(140 150 220 / 0.34);
border-radius: 9px;
background: color-mix(in srgb, #8b6cff 14%, #141830);
color: #eceefb;
font-size: 12px;
font-weight: 700;
cursor: pointer;
user-select: none;
}
/* chevron */
.accordion label::after {
content: '';
width: 6px;
height: 6px;
border-right: 2px solid #949bc0;
border-bottom: 2px solid #949bc0;
transform: translateY(-2px) rotate(45deg);
transition: transform 0.4s;
}
/* the flap: hinged on its top edge, folded back flat (edge-on to the viewer) until opened */
.panel {
position: absolute;
top: calc(100% + 5px);
right: 0;
left: 0;
height: 71px;
padding: 9px 12px;
border: 1px solid color-mix(in srgb, #2ee6d6 60%, transparent);
border-radius: 9px;
background: linear-gradient(180deg, color-mix(in srgb, #2ee6d6 26%, #141830), color-mix(in srgb, #8b6cff 16%, #141830));
color: #eceefb;
font-size: 11px;
line-height: 1.45;
opacity: 0;
pointer-events: none;
transform-origin: top center;
transform: rotateX(-90deg);
transition:
transform 0.5s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s;
}
/* the open section: flap down, header lit (each radio only ever targets its own section) */
#acc-0:checked ~ .sec:nth-of-type(1) .panel,
#acc-1:checked ~ .sec:nth-of-type(2) .panel,
#acc-2:checked ~ .sec:nth-of-type(3) .panel {
opacity: 1;
pointer-events: auto;
transform: rotateX(0deg);
}
#acc-0:checked ~ .sec:nth-of-type(1) label,
#acc-1:checked ~ .sec:nth-of-type(2) label,
#acc-2:checked ~ .sec:nth-of-type(3) label {
border-color: #8b6cff;
background: color-mix(in srgb, #8b6cff 38%, #141830);
}
#acc-0:checked ~ .sec:nth-of-type(1) label::after,
#acc-1:checked ~ .sec:nth-of-type(2) label::after,
#acc-2:checked ~ .sec:nth-of-type(3) label::after {
transform: translateY(1px) rotate(-135deg);
}
#acc-0:focus-visible ~ .sec:nth-of-type(1) label,
#acc-1:focus-visible ~ .sec:nth-of-type(2) label,
#acc-2:focus-visible ~ .sec:nth-of-type(3) label {
outline: 2px solid #2ee6d6;
outline-offset: 2px;
}
/* every section after the open one slides down by exactly one panel's height */
#acc-0:checked ~ .sec:nth-of-type(n + 2) {
transform: translateY(76px);
}
#acc-1:checked ~ .sec:nth-of-type(n + 3) {
transform: translateY(76px);
}
$head: 32px; // header height
$gap: 5px;
$panel: 76px; // panel height
$step: $head + $gap;
// Nothing here animates height. The frame has a fixed size; headers slide with translateY and
// panels swing on their top edge with rotateX, so the whole accordion runs on the compositor.
.d-accordion {
position: relative;
width: 196px;
height: $step * 3 + $panel;
transform-style: preserve-3d;
transform: rotateY(-18deg) rotateX(4deg);
// Real radios keep it keyboard-friendly (arrow keys move between sections); they are only hidden.
> input {
position: absolute;
width: 1px;
height: 1px;
margin: 0;
opacity: 0;
pointer-events: none;
}
&__sec {
position: absolute;
top: calc(var(--i) * #{$step});
right: 0;
left: 0;
height: $head;
transform-style: preserve-3d;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
label {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 12px;
border: 1px solid var(--border-strong);
border-radius: 9px;
background: color-mix(in srgb, var(--accent) 14%, var(--surface-solid));
color: var(--text);
font-size: 12px;
font-weight: 700;
cursor: pointer;
user-select: none;
// chevron
&::after {
content: '';
width: 6px;
height: 6px;
border-right: 2px solid var(--muted);
border-bottom: 2px solid var(--muted);
transform: translateY(-2px) rotate(45deg);
transition: transform 0.4s;
}
}
// The flap: hinged on its top edge, folded back flat (edge-on to the viewer) until opened.
&__panel {
position: absolute;
top: calc(100% + #{$gap});
right: 0;
left: 0;
height: $panel - $gap;
padding: 9px 12px;
border: 1px solid color-mix(in srgb, var(--accent-2) 60%, transparent);
border-radius: 9px;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--accent-2) 26%, var(--surface-solid)),
color-mix(in srgb, var(--accent) 16%, var(--surface-solid))
);
color: var(--text);
font-size: 11px;
line-height: 1.45;
opacity: 0;
pointer-events: none;
transform-origin: top center;
transform: rotateX(-90deg);
transition:
transform 0.5s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s;
}
@for $i from 1 through 3 {
// the open section: flap down, header lit
& > input:nth-of-type(#{$i}):checked ~ &__sec:nth-of-type(#{$i}) {
.d-accordion__panel {
opacity: 1;
pointer-events: auto;
transform: rotateX(0deg);
}
label {
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 38%, var(--surface-solid));
&::after {
transform: translateY(1px) rotate(-135deg);
}
}
}
& > input:nth-of-type(#{$i}):focus-visible ~ &__sec:nth-of-type(#{$i}) label {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
// every section after the open one slides down by exactly one panel
@if $i < 3 {
& > input:nth-of-type(#{$i}):checked ~ &__sec:nth-of-type(n + #{$i + 1}) {
transform: translateY($panel);
}
}
}
}