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.
A greeting card standing half open. Point at it and the cover swings round on its spine, the card turns toward you and a heart pops up out of the fold: a floating layer whose two halves each ride on their own page.
Your edited version
backface-visibility: hidden) with transform-origin: 0 50%, so rotateY(var(--open)) swings it round the spine.pointer-events: none. Hovering only changes three custom properties: the cover angle, the viewing angle and --x0. Every part transitions with the same easing, so they stay in step.translateZ.16px × cot(half the opening angle) from the spine: 14px when the card is half open, 2px when it is flat. That is --x0, and it is why the halves always join into one heart.rotateY(180deg) to face the right half, which mirrors it, so its clip-path is the mirror image of the right half's.transform-origin on the spinepop-up halves = children of each page, translateZ in frontmeeting line --x0 = lift × cot(angle ÷ 2)static hit target, card inside moves<div class="scene">
<div class="greeting" tabindex="0">
<div class="card">
<i class="shadow"></i>
<div class="page"><b>Happy<br>day!</b><span></span><span></span><small>with love, C.</small></div>
<i class="pop"></i>
<div class="cover">
<div class="front"><b>For<br>you</b></div>
<div class="inside"><small>open me</small></div>
<i class="pop pop-left"></i>
</div>
</div>
</div>
</div>
.scene {
perspective: 800px;
}
/* the static hit target: hovering it only changes numbers */
.greeting {
--open: -96deg; /* cover angle: half open */
--view: 40deg; /* how far round we look at the card */
--x0: 14px; /* where the two heart halves meet (see below) */
display: grid;
place-items: center;
width: 210px;
height: 180px;
outline: none;
cursor: pointer;
transform-style: preserve-3d;
}
.greeting:hover,
.greeting:focus-visible {
--open: -166deg; /* almost flat, so it still stands */
--view: 24deg; /* the card turns to face you as it opens */
--x0: 2px;
}
/* the card's left edge is the spine: move it to the middle, look from above and the left */
.card {
position: relative;
width: 92px;
height: 124px;
pointer-events: none;
transform-style: preserve-3d;
transform-origin: 0 50%;
transform: translateX(46px) rotateX(-18deg) rotateY(var(--view));
transition: transform 0.9s cubic-bezier(0.3, 1.15, 0.45, 1);
}
/* a soft shadow lying on the table */
.shadow {
position: absolute;
top: 84px;
left: -101px;
width: 202px;
height: 80px;
background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
transform: rotateX(90deg);
}
/* the inside right page: it stays put */
.page {
position: absolute;
inset: 0;
display: grid;
align-content: start;
gap: 6px;
padding: 68px 10px 0 16px;
border-radius: 0 6px 6px 0;
background:
radial-gradient(circle at 78% 16%, #ffb547 0 2.5px, transparent 3.5px),
radial-gradient(circle at 60% 9%, #2ee6d6 0 2px, transparent 3px),
radial-gradient(circle at 88% 30%, #ff4d9d 0 2px, transparent 3px),
linear-gradient(90deg, #e4ddcf, #fbf7ef 18%);
color: #5a44b8;
backface-visibility: hidden;
}
.page b { font-size: 17px; font-weight: 800; line-height: 0.95; }
.page span { height: 3px; border-radius: 2px; background: rgb(20 20 40 / 0.16); }
.page span:nth-of-type(2) { width: 70%; }
.page small { color: #6b6478; font-size: 7px; font-style: italic; }
/* the heart's shadow on the page, stronger once it has popped up */
.page::before {
content: '';
position: absolute;
top: 18px;
left: 8px;
width: 40px;
height: 46px;
border-radius: 50%;
background: radial-gradient(closest-side, rgb(40 10 50 / 0.4), transparent);
opacity: 0.4;
transition: opacity 0.9s;
}
/* the half-open cover shades the page */
.page::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(90deg, rgb(20 10 40 / 0.55), rgb(20 10 40 / 0.1) 85%);
transition: opacity 0.9s;
}
.greeting:is(:hover, :focus-visible) .page::before { opacity: 1; }
.greeting:is(:hover, :focus-visible) .page::after { opacity: 0.12; }
/* The pop-up is a floating layer: each half stays parallel to its own page, 16px in front of it.
Two such planes meet 16px × cot(half the opening angle) from the spine: that is --x0. */
.pop {
position: absolute;
top: 12px;
left: 0;
width: 28px;
height: 48px;
background:
radial-gradient(circle at 50% 22%, rgb(255 255 255 / 0.75) 0 2.5px, transparent 6px),
linear-gradient(170deg, #ff4d9d, #ff817a);
/* the right half of a heart: its middle line is the left edge */
clip-path: path('M0 9 C 3 3 7 0 12 0 C 21 0 28 6 28 16 C 28 28 16 38 0 48 Z');
transform: translate3d(var(--x0), 0, 16px);
transition: transform 0.9s cubic-bezier(0.3, 1.15, 0.45, 1);
}
/* the left half rides on the cover's inside, turned round to face the right half
(turning it round mirrors it, so its middle line is drawn on its right edge) */
.pop-left {
clip-path: path('M28 9 C 25 3 21 0 16 0 C 7 0 0 6 0 16 C 0 28 12 38 28 48 Z');
transform: translate3d(var(--x0), 0, -16px) rotateY(180deg);
}
/* the front cover: two faces back to back, hinged on the spine */
.cover {
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform-origin: 0 50%;
transform: translateZ(2px) rotateY(var(--open));
transition: transform 0.9s cubic-bezier(0.3, 1.15, 0.45, 1);
}
.front,
.inside {
position: absolute;
inset: 0;
display: grid;
border-radius: 0 6px 6px 0;
backface-visibility: hidden;
}
.front {
place-items: center;
background:
radial-gradient(circle at 22% 18%, rgb(255 255 255 / 0.75) 0 2px, transparent 3px),
radial-gradient(circle at 76% 26%, #ffb547 0 3px, transparent 4px),
radial-gradient(circle at 30% 80%, #2ee6d6 0 2.5px, transparent 3.5px),
linear-gradient(150deg, #8b6cff, #ff4d9d);
}
.front b {
padding: 10px 12px;
border: 2px solid rgb(255 255 255 / 0.85);
border-radius: 50%;
color: #fff;
font-size: 15px;
line-height: 1;
text-align: center;
}
/* the inside of the cover, seen once it swings past 90° */
.inside {
place-items: end center;
padding-bottom: 14px;
border-radius: 6px 0 0 6px;
background:
repeating-linear-gradient(135deg, rgb(139 108 255 / 0.12) 0 6px, transparent 6px 12px),
linear-gradient(270deg, #e4ddcf, #fbf7ef 18%);
color: #5a44b8;
font-size: 7px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
transform: rotateY(180deg);
}
$w: 92px; // one page of the card
$h: 124px;
$rest: -96deg; // half open: the cover stands at a right angle to the page
$open: -166deg; // almost flat, so it still stands
$ease: 0.9s cubic-bezier(0.3, 1.15, 0.45, 1);
$paper: #fbf7ef;
$lift: 16px; // how far the pop-up layer floats in front of the pages
// The root is the static hit target; the card inside is pointer-events: none and free to move.
.d-greeting {
--open: #{$rest};
--view: 40deg; // how far round we look at the card
--x0: 14px;
display: grid;
place-items: center;
width: 210px;
height: 180px;
outline: none;
cursor: pointer;
transform-style: preserve-3d;
&:hover,
&:focus-visible {
--open: #{$open};
--view: 24deg; // the card turns to face you as it opens
--x0: 2px;
}
// The card's left edge is the spine. Move the spine to the middle, then look at the card from
// a little above (rotateX) and from the left (rotateY), turning around the spine.
&__card {
position: relative;
width: $w;
height: $h;
pointer-events: none;
transform-style: preserve-3d;
transform-origin: 0 50%;
transform: translateX($w * 0.5) rotateX(-18deg) rotateY(var(--view));
transition: transform $ease;
}
// a soft shadow lying on the table under the card
&__shadow {
position: absolute;
top: $h - 40px;
left: -$w * 1.1;
width: $w * 2.2;
height: 80px;
background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
transform: rotateX(90deg);
}
// the inside right page: it stays put
&__page {
position: absolute;
inset: 0;
display: grid;
align-content: start;
gap: 6px;
padding: 68px 10px 0 16px;
border-radius: 0 6px 6px 0;
background:
radial-gradient(circle at 78% 16%, var(--warm) 0 2.5px, transparent 3.5px),
radial-gradient(circle at 60% 9%, var(--accent-2) 0 2px, transparent 3px),
radial-gradient(circle at 88% 30%, var(--hot) 0 2px, transparent 3px),
linear-gradient(90deg, #e4ddcf, $paper 18%);
box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.08);
color: color-mix(in srgb, var(--accent) 80%, #000);
backface-visibility: hidden;
b {
font-size: 17px;
font-weight: 800;
line-height: 0.95;
}
span {
height: 3px;
border-radius: 2px;
background: rgb(20 20 40 / 0.16);
&:nth-of-type(2) {
width: 70%;
}
}
small {
color: #6b6478;
font-size: 7px;
font-style: italic;
}
// the shadow the standing heart throws onto the page, stronger as it stands up
&::before {
content: '';
position: absolute;
top: 18px;
left: 8px;
width: 40px;
height: 46px;
border-radius: 50%;
background: radial-gradient(closest-side, rgb(40 10 50 / 0.4), transparent);
opacity: 0.4;
transition: opacity $ease;
}
// the half-open cover shades the page; it brightens as the card opens
&::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(90deg, rgb(20 10 40 / 0.55), rgb(20 10 40 / 0.1) 85%);
transition: opacity $ease;
}
}
// The pop-up: a floating layer. Each half of the heart stays parallel to its own page, $lift
// in front of it, and the two halves meet on an inner fold, like a smaller card inside the card.
// Where they meet (--x0 from the spine) is $lift × cot(half the opening angle): 14px when the
// card is half open, 2px when it is flat. Only that number and the cover's angle change.
&__pop {
position: absolute;
top: 12px;
left: 0;
width: 28px;
height: 48px;
background:
radial-gradient(circle at 50% 22%, rgb(255 255 255 / 0.75) 0 2.5px, transparent 6px),
linear-gradient(170deg, var(--hot), color-mix(in srgb, var(--hot) 50%, var(--warm)));
// the right half of a heart: its middle line is the left edge, on the inner fold
clip-path: path('M0 9 C 3 3 7 0 12 0 C 21 0 28 6 28 16 C 28 28 16 38 0 48 Z');
transform: translate3d(var(--x0), 0, $lift);
transition: transform $ease;
// the left half rides on the cover, on its inside, and is turned round to face the right half
// (turning it round mirrors it, so its middle line is drawn on its right edge)
&--l {
clip-path: path('M28 9 C 25 3 21 0 16 0 C 7 0 0 6 0 16 C 0 28 12 38 28 48 Z');
transform: translate3d(var(--x0), 0, -$lift) rotateY(180deg);
}
}
// The front cover: two faces back to back, hinged on the spine.
&__cover {
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform-origin: 0 50%;
transform: translateZ(2px) rotateY(var(--open));
transition: transform $ease;
}
&__front,
&__inside {
position: absolute;
inset: 0;
border-radius: 0 6px 6px 0;
backface-visibility: hidden;
}
&__front {
display: grid;
place-items: center;
background:
radial-gradient(circle at 22% 18%, rgb(255 255 255 / 0.75) 0 2px, transparent 3px),
radial-gradient(circle at 76% 26%, var(--warm) 0 3px, transparent 4px),
radial-gradient(circle at 30% 80%, var(--accent-2) 0 2.5px, transparent 3.5px),
radial-gradient(circle at 80% 76%, rgb(255 255 255 / 0.6) 0 2px, transparent 3px),
linear-gradient(150deg, var(--accent), var(--hot));
box-shadow: inset 3px 0 0 rgb(0 0 0 / 0.15);
b {
padding: 10px 12px;
border: 2px solid rgb(255 255 255 / 0.85);
border-radius: 50%;
color: #fff;
font-size: 15px;
font-weight: 800;
line-height: 1;
text-align: center;
}
}
// the inside of the cover, seen once it swings past 90°
&__inside {
display: grid;
place-items: end center;
padding-bottom: 14px;
border-radius: 6px 0 0 6px;
background:
repeating-linear-gradient(135deg, color-mix(in srgb, var(--accent) 12%, transparent) 0 6px, transparent 6px 12px),
linear-gradient(270deg, #e4ddcf, $paper 18%);
color: color-mix(in srgb, var(--accent) 70%, #000);
transform: rotateY(180deg);
small {
font-size: 7px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
}
&:hover &__page::after,
&:focus-visible &__page::after {
opacity: 0.12;
}
&:hover &__page::before,
&:focus-visible &__page::before {
opacity: 1;
}
}