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
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 {
perspective: 800px;
}
.table {
display: grid;
grid-template-columns: repeat(2, 100px);
grid-auto-rows: 80px;
transform-style: preserve-3d;
transform: translateY(12px) rotateX(24deg); /* how far the "table" leans back */
/* 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: 15px; --y: 9px; --z: 2px; --r: -10deg; }
.slot:nth-child(2) { --x: -12px; --y: 5px; --z: 6px; --r: 7deg; }
.slot:nth-child(3) { --x: 11px; --y: -8px; --z: 10px; --r: 5deg; }
.slot:nth-child(4) { --x: -15px; --y: -10px; --z: 14px; --r: -8deg; }
/* contact shadow: stays on the table and fades as the photo lifts */
.slot::before {
content: '';
position: absolute;
top: calc(50% - 44px);
left: calc(50% - 36px);
width: 72px;
height: 88px;
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, 10px) rotateZ(0deg) scale(1.15);
}
.photo {
position: absolute;
top: calc(50% - 44px);
left: calc(50% - 36px);
width: 72px;
height: 88px;
padding: 5px 5px 0;
border-radius: 3px;
background: linear-gradient(170deg, #fbf9f4, #e6e1d6);
box-shadow: 0 1px 2px 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: 62px;
border-radius: 1px;
}
.photo small {
display: block;
color: #4a4658;
font-size: 8px;
font-weight: 600;
line-height: 21px;
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(0px, 6px, 52px) rotateX(-24deg) rotateZ(0deg) scale(1.1);
}
$tilt: 24deg; // how far the "table" leans back
.d-polaroid {
display: grid;
grid-template-columns: repeat(2, 100px);
grid-auto-rows: 80px;
transform-style: preserve-3d;
transform: translateY(12px) rotateX($tilt);
// 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
&:nth-child(1) {
--x: 15px;
--y: 9px;
--z: 2px;
--r: -10deg;
}
&:nth-child(2) {
--x: -12px;
--y: 5px;
--z: 6px;
--r: 7deg;
}
&:nth-child(3) {
--x: 11px;
--y: -8px;
--z: 10px;
--r: 5deg;
}
&:nth-child(4) {
--x: -15px;
--y: -10px;
--z: 14px;
--r: -8deg;
}
// contact shadow: stays on the table and fades as the photo lifts
&::before {
content: '';
position: absolute;
top: calc(50% - 44px);
left: calc(50% - 36px);
width: 72px;
height: 88px;
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;
}
&:hover::before,
&:focus-visible::before {
opacity: 0.35;
transform: translate(0, 10px) rotateZ(0deg) scale(1.15);
}
}
&__photo {
position: absolute;
top: calc(50% - 44px);
left: calc(50% - 36px);
width: 72px;
height: 88px;
padding: 5px 5px 0;
border-radius: 3px;
background: linear-gradient(170deg, #fbf9f4, #e6e1d6);
box-shadow: 0 1px 2px 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);
i {
display: block;
height: 62px;
border-radius: 1px;
}
small {
display: block;
color: #4a4658;
font-size: 8px;
font-weight: 600;
line-height: 21px;
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, var(--accent) 0%, var(--hot) 55%, var(--warm) 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%, var(--accent-2) 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, var(--hot));
}
&__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(0px, 6px, 52px) rotateX(-$tilt) rotateZ(0deg) scale(1.1);
}
}