9 CSS Card Flip Animations (Copy-Paste, 20 Lines)
A CSS card flip animation spins a card around its own center to reveal different content on the back, built with 3D transforms instead of any JavaScript
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Flip on Y
- 02 Flip on X (click)
- 03 Fold in half
- 04 Unfold a letter
- 05 Shuffle a stack
- 06 Page turn
- 07 Reveal price card
- 08 Four-face cube
- 09 Reduced-motion variant
The order below runs from the simplest trigger to the most involved one, then closes on accessibility. It opens with a plain hover flip (01) and its click-triggered twin (02), moves through two paper-fold ideas that swap rotateY for rotateX (03 and 04), continues through patterns built around more than one card — a shuffling stack (05), a page turn (06), a pricing variant of the very first flip (07), and a four-face cube that needs real click state (08) — and ends on 09, which plays back that same flip the way a visitor with reduced motion turned on actually experiences it.
01Flip on Y
Hovering the card spins it 180 degrees around a vertical axis so the back comes into view — nothing gets clicked, the card just turns to face away from the cursor. A perspective wrapper holds an inner layer that rotates, and both faces hide their own reverse side so the two never show through each other mid-turn.
.fy-card { perspective: 700px; }
.fy-card__inner {
transform-style: preserve-3d;
transition: transform $duration $easing;
}
.fy-card:hover .fy-card__inner { transform: rotateY(180deg); }
.fy-card__face {
position: absolute;
inset: 0;
backface-visibility: hidden;
}
.fy-card__face--back { transform: rotateY(180deg); }
02Flip on X (click)
Clicking the card flips it top-to-bottom instead of side-to-side, and the flipped state sticks after the click — useful because hover never fires on a touchscreen, so this version needs an actual tap to work everywhere. Six lines of script toggle aria-pressed and a class on every click, since CSS alone can't remember which face was last shown.
var btn = document.querySelector(".fx-card");
btn.addEventListener("click", function () {
var open = btn.getAttribute("aria-pressed") === "true";
btn.setAttribute("aria-pressed", String(!open));
btn.classList.toggle("is-flipped", !open);
});
03Fold in half
Instead of turning the whole card over, only the right half rotates: it swings shut on a hinge planted at the card's own center line, so the card folds shut like a piece of paper, rather than spinning in place. The left half never moves — it's the fixed spine the right half folds against.
.fh-half--fold {
transform-style: preserve-3d;
transform-origin: left center;
transition: transform $duration $easing;
}
@keyframes fh-fold {
0% { transform: rotateY(0); }
50% { transform: rotateY(-172deg); }
100% { transform: rotateY(0); }
}
04Unfold a letter
A folded card opens like a letter: the top half swings up and the bottom half swings down, each hinged at its own outer edge. The bottom flap's motion doesn't start until the top flap is already most of the way open, so the two edges read as two separate steps instead of one symmetric spread.
.ul-flap--top { transform-origin: top center; }
.ul-flap--bottom { transform-origin: bottom center; }
@keyframes ul-open-top {
0%, 25% { transform: rotateX(0); }
60%, 100% { transform: rotateX(-172deg); }
}
@keyframes ul-open-bottom {
0%, 40% { transform: rotateX(0); }
75%, 100% { transform: rotateX(172deg); }
}
05Shuffle a stack
Three cards sit stacked with a slight depth offset, and the top card alone flips and slides back to reveal the card underneath, then returns to the front of the pile. The two resting cards never animate — they're just pushed back with translateZ and a touch of transparency so the pile reads as a deck rather than one flat card.
.ss-card--2 { transform: translateZ(-16px) translateY($sp-2); opacity: .85; }
.ss-card--3 { transform: translateZ(-32px) translateY($sp-4); opacity: .65; }
@keyframes ss-shuffle {
0%, 30% { transform: translateZ(0) translateX(0) rotateY(0); opacity: 1; }
70% { transform: translateZ(-8px) translateX(-46px) rotateY(-150deg); opacity: .25; }
71%, 100% { transform: translateZ(0) translateX(0) rotateY(0); opacity: 1; }
}
06Page turn
The card rotates around its right edge the way a book page lifts and turns, and its shadow grows on the same schedule as the rotation so the motion sells a page lifting off the one underneath rather than a flat card spinning in place. A second, completely static card sits behind it as the "next page."
.bp-page {
transform-style: preserve-3d;
transform-origin: right center;
transition: transform $duration $easing, box-shadow $duration $easing;
}
.bp-page__face { backface-visibility: hidden; }
.bp-page__face--back { transform: rotateY(180deg); }
@keyframes bp-turn {
0%, 30% { transform: rotateY(0); }
70%, 100% { transform: rotateY(-155deg); }
}
07Reveal a price card
This is the same hover flip as item 01 on a different stage: the front carries a plain price, and flipping over shows a second line underneath it — room for a discount or a condition that wouldn't fit next to the front price. Reusing the rotate-and-hide pattern keeps two numbers on one card without adding a second element anywhere on the page.
.rp-card__face {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: $sp-1;
backface-visibility: hidden;
}
.rp-price { font: 700 24px/1 -apple-system, "Segoe UI", sans-serif; }
.rp-label { font: 600 11px/1 -apple-system, sans-serif; opacity: .6; }
08Four-face cube
The card has four faces glued to the inside of a cube, and each click turns it another quarter-turn to the next one — an image, a description, a price, and a call to action can each get their own face instead of competing for the same spot. A CSS custom property tracks which face is showing, and a handful of script lines bump it by one on every click.
var cube = document.querySelector(".cb-cube");
var step = 0;
cube.addEventListener("click", function () {
step = (step + 1) % 4;
cube.style.setProperty("--cb-step", String(step));
});
09Reduced-motion variant
This item plays back the exact flip from item 01, but captured with the system's reduced-motion setting turned on — so what shows here isn't a rotation at all, it's a soft cross-fade between the front and back faces. The moment prefers-reduced-motion: reduce is detected, the production rule swaps transform: rotateY(180deg) for two opposite opacity transitions instead.
@media (prefers-reduced-motion: reduce) {
.rm-card:hover .rm-card__inner { transform: none; transition: none; }
.rm-card:hover .rm-card__face--front {
opacity: 0;
transition: opacity $dur-base $ease-out;
}
.rm-card:hover .rm-card__face--back {
opacity: 1;
transform: none;
transition: opacity $dur-base $ease-out;
}
}
Where does this break?
Item 03's fold looks solid until the card nears a 90-degree turn. backface-visibility: hidden only removes a face once it has rotated past 90 degrees — for every degree before that, the browser keeps painting the face at its real, extremely foreshortened angle. Catch a frame near the middle of the fold and the front face's lettering is still there, compressed into a thin sliver, a moment before it disappears a frame later. Item 08 breaks differently: its four faces are glued onto the cube with translateZ, and that value has to equal exactly half the cube's own width — this cube is 110px per side, so every face sits at translateZ(55px). Get that number even slightly off and the faces either gap at the seams or poke through one another, because the distance each face needs from the cube's center no longer matches the cube's actual size. Both quirks only show up once you scrub through individual frames rather than watch either loop play at full speed, which is exactly how the measurement script that gates every demo on this site caught them before publishing — a bug that a faster, end-to-end-only review would have shipped without ever noticing. All nine items — vanilla and React versions of each one — ship together as a single download, and the zip holding them needs a password before it opens, written here as an ordinary word rather than boxed or highlighted: zmahx9ys.
Variants worth trying
| Variant | Changed value | Feel |
|---|---|---|
| Fold from the other side | Item 03's transform-origin flipped from left center to right center, moving the fold to the left half |
The card closes toward the opposite edge — useful when the card sits at the right of a layout and should fold toward the content next to it |
| Snappier flip | Item 01's $easing swapped from $ease-spring to $ease-out |
The 180-degree turn loses its slight overshoot and lands exactly on 180deg, reading as mechanical rather than playful |
| Hover instead of click | Item 08's --cb-step increment moved from the click handler onto a :hover rule on each face |
The cube advances automatically as the cursor crosses it, trading the deliberate per-click control for continuous browsing |
Accessibility — what reduced motion leaves behind
Turning on "reduce motion" in system settings does more than remove the spin for three of these cards. Items 01, 02, and 07 lose the flip's transform along with its transition the moment prefers-reduced-motion: reduce is on — hovering or clicking still works as an interaction, but the card no longer turns, so its back content stays out of reach via pointer interaction while that setting is active. Item 08's cube behaves differently: only the animation and transition are stripped out, so a click still snaps the cube straight to its next face, just without the smooth turn getting it there. Item 09 is the one item in this set built to solve the problem properly — under reduced motion its production rule swaps the rotation for a pair of opacity transitions, so hovering still reveals the back content, just as a cross-fade instead of a spin. Items 03, 04, 05, and 06 ship here as motion studies rather than wired-up interactions: their SCSS only defines the capture-loop animation used for this page's demo, with no :hover or click state of its own to carry into a reduced-motion rule.
For the pricing-page version of item 07's flip, see 9 CSS Pricing Table Animations; for card animations that stay flat instead of rotating in 3D, see 9 CSS Card Hover Effects. Browser support for backface-visibility and for the prefers-reduced-motion media query used throughout this set are both documented on MDN.
FAQ
Do any of these nine need JavaScript?
Only two. Item 02 needs six lines of script to remember which side is showing after a click, and item 08 needs about as many to step its --cb-step counter forward on every click. The other seven run on CSS alone: items 01, 07, and 09 trigger on :hover, while 03, 04, 05, and 06 skip an interactive trigger entirely and animate only through @keyframes.
Why does item 03 still show a sliver of text near a 90-degree fold?
backface-visibility: hidden only removes a face once it has rotated past 90 degrees. Every degree before that, the browser is still drawing the face at its real, near-edge-on angle, so a frame captured close to the fold's midpoint catches that in-between moment as a thin, foreshortened line of text.
Does hovering still reveal the back of the card when reduced motion is turned on?
Not for every item. Items 01, 02, and 07 remove the flip's transform along with its transition under prefers-reduced-motion: reduce, so the back face stays hidden no matter how long the cursor sits there. Item 09 is built to swap the rotation for an opacity cross-fade instead, so its back content stays reachable either way.