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.
Four photos lying on a tilted table at different angles and heights. Point at one and it rises off the table, straightens and turns to face you, leaving its shadow behind.
Your edited version
--u, tied to the canvas, so the table, the photos and their captions are the same share of a gallery card, the editor and a recording canvas. The numbers below are those units.rotateX(24deg) on the container). Only the slots catch the pointer — the photos inside are pointer-events: none.--x / --y / --z / --r so the scatter is uneven and the four never sit exactly on top of each other.translate3d rotateX rotateZ scale), just with different numbers — same-shape lists let the browser interpolate each value smoothly instead of jumping.rotateX(-24deg) to cancel the table's own tilt so the photo squares up to the viewer instead of lifting at an angle.::before pseudo-element is the contact shadow: it stays glued to the table and only fades and grows, never lifts, which sells the photo as airborne.tilted plane + counter-rotation on hovertranslateZ = height above the tablestatic slots as hit targetsmatching transform lists for clean interpolation<div class="scene">
<div class="table">
<div class="slot" tabindex="0"><div class="photo"><i></i><small>sunset</small></div></div>
<div class="slot" tabindex="0"><div class="photo"><i></i><small>seaside</small></div></div>
<div class="slot" tabindex="0"><div class="photo"><i></i><small>peaks</small></div></div>
<div class="slot" tabindex="0"><div class="photo"><i></i><small>midnight</small></div></div>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the table is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.32vmin;
perspective: calc(800 * var(--u));
}
.table {
display: grid;
grid-template-columns: repeat(2, calc(100 * var(--u)));
grid-auto-rows: calc(80 * var(--u));
transform-style: preserve-3d;
/* how far the "table" leans back */
transform: translateY(calc(12 * var(--u))) rotateX(24deg);
/* the table and its four slots are coplanar: only the slots may catch the pointer */
pointer-events: none;
}
/* a slot is a static quarter of the table: the hit target. The photo inside it is free to fly */
.slot {
position: relative;
outline: none;
cursor: pointer;
pointer-events: auto;
transform-style: preserve-3d;
}
/* each photo is pushed toward the middle so they overlap, and sits a little higher than the
previous one so overlapping photos never z-fight */
.slot:nth-child(1) { --x: calc(15 * var(--u)); --y: calc(9 * var(--u)); --z: calc(2 * var(--u)); --r: -10deg; }
.slot:nth-child(2) { --x: calc(-12 * var(--u)); --y: calc(5 * var(--u)); --z: calc(6 * var(--u)); --r: 7deg; }
.slot:nth-child(3) { --x: calc(11 * var(--u)); --y: calc(-8 * var(--u)); --z: calc(10 * var(--u)); --r: 5deg; }
.slot:nth-child(4) { --x: calc(-15 * var(--u)); --y: calc(-10 * var(--u)); --z: calc(14 * var(--u)); --r: -8deg; }
/* contact shadow: stays on the table and fades as the photo lifts */
.slot::before {
content: '';
position: absolute;
top: calc(50% - 44 * var(--u));
left: calc(50% - 36 * var(--u));
width: calc(72 * var(--u));
height: calc(88 * var(--u));
background: radial-gradient(ellipse, rgb(0 0 0 / 0.6) 30%, transparent 72%);
pointer-events: none;
transform: translate(var(--x), var(--y)) rotateZ(var(--r)) scale(1);
transition:
transform 0.5s,
opacity 0.5s;
}
.slot:hover::before,
.slot:focus-visible::before {
opacity: 0.35;
transform: translate(0, calc(10 * var(--u))) rotateZ(0deg) scale(1.15);
}
.photo {
position: absolute;
top: calc(50% - 44 * var(--u));
left: calc(50% - 36 * var(--u));
/* 72 × 88 outside, the white border included: the picture (62) and caption (21) fill it, and
the contact shadow under it is the same box */
box-sizing: border-box;
width: calc(72 * var(--u));
height: calc(88 * var(--u));
padding: calc(5 * var(--u)) calc(5 * var(--u)) 0;
border-radius: calc(3 * var(--u));
background: linear-gradient(170deg, #fbf9f4, #e6e1d6);
box-shadow: 0 calc(1 * var(--u)) calc(2 * var(--u)) rgb(0 0 0 / 0.5);
pointer-events: none;
/* same function list in both states, so the browser interpolates number by number */
transform: translate3d(var(--x), var(--y), var(--z)) rotateX(0deg) rotateZ(var(--r)) scale(1);
transition: transform 0.5s cubic-bezier(0.3, 1.3, 0.5, 1);
}
.photo i {
display: block;
height: calc(62 * var(--u));
border-radius: calc(1 * var(--u));
}
.photo small {
display: block;
color: #4a4658;
font-size: calc(9 * var(--u));
font-weight: 600;
line-height: calc(21 * var(--u));
text-align: center;
}
/* the "photos" are just layered gradients */
.slot:nth-child(1) i {
background:
radial-gradient(circle at 50% 78%, #fff6c8 0 9%, transparent 10%),
linear-gradient(180deg, #8b6cff 0%, #ff4d9d 55%, #ffb547 78%, #2a1840 79%);
}
.slot:nth-child(2) i {
background:
radial-gradient(circle at 72% 26%, #fff 0 8%, transparent 9%),
linear-gradient(180deg, #7fd8ff 0 52%, #2ee6d6 53%, #0d6f8a 100%);
}
.slot:nth-child(3) i {
background:
conic-gradient(from 150deg at 35% 30%, #3b2a78 0 60deg, transparent 0),
conic-gradient(from 145deg at 68% 45%, #5a3fb0 0 70deg, transparent 0),
linear-gradient(180deg, #ffd9a0, #ff4d9d);
}
.slot:nth-child(4) i {
background:
radial-gradient(circle at 28% 30%, #fff 0 2%, transparent 3%),
radial-gradient(circle at 62% 18%, #fff 0 1.5%, transparent 2.5%),
radial-gradient(circle at 80% 48%, #fff 0 2%, transparent 3%),
radial-gradient(circle at 44% 58%, #fff 0 1.5%, transparent 2.5%),
radial-gradient(circle at 74% 74%, #f4f1d0 0 10%, transparent 11%),
linear-gradient(180deg, #0b0d2a, #3a2a7a);
}
/* lift along the table's normal, then undo the table's tilt so the photo faces the viewer */
.slot:hover .photo,
.slot:focus-visible .photo {
transform: translate3d(0, calc(6 * var(--u)), calc(52 * var(--u))) rotateX(-24deg) rotateZ(0deg) scale(1.1);
}