A flipping card is a compact way to reveal secondary content without sending users to a new page or stuffing text into cramped spaces. By the end of this article you will build a polished, accessible flipping card that works with hover, focus, and an optional class toggle. You will learn the 3D transform setup, the front and back face layering, motion preferences, and a few production tweaks that prevent common visual glitches.
Why a Flipping Card Effect Matters
The flip pattern shines when you have a clear primary summary and a short detail view. Think product teasers, team member profiles, and feature highlights. A CSS-driven flip keeps the DOM lean, avoids layout shifts, and runs on the GPU through transforms. You can ship a refined interaction without a JavaScript framework. It is also a great entry point into perspective, preserve-3d, and backface-visibility, which you can reuse for tooltips, carousels, and onboarding cards.
Prerequisites
You will need basic layout knowledge and comfort with positioning. The steps below use custom properties, pseudo-elements, and a few modern selectors.
- Basic HTML
- CSS custom properties
- CSS pseudo-elements (::before / ::after)
Step 1: The HTML Structure
Here is the complete HTML. The key is a container that provides perspective, an inner wrapper that rotates, and two faces that sit back-to-back. The front face holds the summary. The back face holds the details and any call to action. We will support hover and focus by default, and we will leave room for an optional class toggle for tap devices or custom triggers.
<div class="card-grid">
<article class="flip-card" aria-label="Alpine Lake card" role="group" tabindex="0">
<div class="flip-card__inner">
<div class="flip-card__face flip-card__front">
<img class="flip-card__media" src="alpine.jpg" alt="Alpine lake at sunrise">
<h3 class="flip-card__title">Alpine Lake</h3>
<p class="flip-card__excerpt">A quick escape with crisp air and quiet trails.</p>
<button class="flip-card__hint" type="button" aria-label="Show more">Details</button>
</div>
<div class="flip-card__face flip-card__back">
<h3 class="flip-card__title">Plan Your Visit</h3>
<ul class="flip-card__list">
<li>Best season: May-September</li>
<li>Trail length: 6.4 km loop</li>
<li>Permit: Day pass on weekends</li>
</ul>
<a class="flip-card__cta" href="#">Get the guide</a>
</div>
</div>
</article>
<!-- Duplicate cards as needed -->
<article class="flip-card" aria-label="Coastal Trail card" role="group" tabindex="0">
<div class="flip-card__inner">
<div class="flip-card__face flip-card__front">
<img class="flip-card__media" src="coast.jpg" alt="Rocky coast with waves">
<h3 class="flip-card__title">Coastal Trail</h3>
<p class="flip-card__excerpt">Sea breeze, sandstone cliffs, and tide pools.</p>
<button class="flip-card__hint" type="button" aria-label="Show more">Details</button>
</div>
<div class="flip-card__face flip-card__back">
<h3 class="flip-card__title">What to Bring</h3>
<ul class="flip-card__list">
<li>Windbreaker</li>
<li>Trail shoes</li>
<li>Water and snacks</li>
</ul>
<a class="flip-card__cta" href="#">See packing list</a>
</div>
</div>
</article>
</div>
Step 2: The Basic CSS & Styling
Start with root variables for color, radius, and timing. Set up a centered grid to showcase several cards. Each card uses a fixed width with aspect-ratio for a consistent footprint. Keep borders, shadows, and padding light so the flip reads cleanly.
/* CSS */
:root {
--card-w: 320px;
--card-ratio: 3 / 4;
--radius: 16px;
--surface: #0f172a;
--elev: #111827;
--text: #e5e7eb;
--muted: #9ca3af;
--brand: #34d399;
--shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
--speed: 700ms;
--easing: cubic-bezier(.2,.7,.3,1);
}
*,
*::before,
*::after { box-sizing: border-box; }
body {
margin: 0;
font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
color: var(--text);
background: radial-gradient(1200px 800px at 20% 10%, #1f2937, #0b0f1a);
display: grid;
place-items: center;
min-height: 100svh;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--card-w)), 1fr));
gap: 24px;
width: min(1100px, 100%);
padding: 32px;
}
.flip-card {
width: var(--card-w);
aspect-ratio: var(--card-ratio);
position: relative;
perspective: 1200px;
border-radius: var(--radius);
box-shadow: var(--shadow);
outline: none;
}
.flip-card:focus-visible {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 60%, transparent), var(--shadow);
}
.flip-card__inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform var(--speed) var(--easing);
border-radius: inherit;
}
.flip-card__face {
position: absolute;
inset: 0;
display: grid;
align-content: start;
gap: 12px;
padding: 16px;
border-radius: inherit;
background: var(--surface);
backface-visibility: hidden;
overflow: clip;
}
.flip-card__front {
background:
linear-gradient(180deg, rgba(255,255,255,.06), transparent 150px),
var(--elev);
}
.flip-card__back {
transform: rotateY(180deg);
background: linear-gradient(180deg, rgba(52,211,153,.08), transparent 150px), var(--surface);
}
.flip-card__media {
width: 100%;
height: 52%;
object-fit: cover;
border-radius: calc(var(--radius) - 4px);
}
.flip-card__title {
margin: 4px 0 0 0;
font-size: 1.125rem;
}
.flip-card__excerpt {
margin: 0;
color: var(--muted);
}
.flip-card__list {
margin: 8px 0 16px;
padding-left: 18px;
}
.flip-card__cta,
.flip-card__hint {
align-self: end;
justify-self: start;
background: var(--brand);
color: #032c20;
border: none;
padding: 10px 14px;
border-radius: 10px;
text-decoration: none;
font-weight: 600;
cursor: pointer;
}
.flip-card__cta:hover,
.flip-card__hint:hover {
filter: saturate(1.1);
}
.flip-card__cta:focus-visible,
.flip-card__hint:focus-visible {
outline: 3px solid color-mix(in srgb, var(--brand) 45%, white 10%);
outline-offset: 2px;
}
Advanced Tip: Define sizing and timing with CSS variables. A flip that feels sluggish at 700ms can snap at 500ms on a dense grid. By centralizing these values, you can tune motion and theming across many cards in seconds.
Step 3: Building the Card Faces
Now wire up the 3D illusion. The container provides perspective. The inner wrapper creates a 3D space that can rotate as one unit. Each face is absolutely positioned and fills the card. The back face starts rotated 180 degrees on the Y axis so it can swing into view when the inner wrapper flips.
/* CSS */
.flip-card__front::after {
/* subtle gloss that moves on hover in Advanced Techniques */
content: "";
position: absolute;
inset: 0;
background: linear-gradient(120deg, rgba(255,255,255,.18), transparent 35%, transparent 65%, rgba(255,255,255,.06));
mix-blend-mode: soft-light;
opacity: .35;
pointer-events: none;
}
.flip-card__back {
display: grid;
align-content: start;
}
.flip-card__back .flip-card__title {
margin-top: 8px;
}
How This Works (Code Breakdown)
The perspective on .flip-card sets a vanishing point for its children. A value around 800-1400px reads natural for card-sized elements. The smaller the number, the more dramatic the depth. The .flip-card__inner uses transform-style: preserve-3d so its children keep their own depth when the inner wrapper rotates. That prevents the faces from flattening.
backface-visibility: hidden keeps mirrored text from showing through when the faces rotate away. Without it, you will see reverse text for a brief moment. The back face is rotated 180deg so it starts facing away. When the inner wrapper flips, the back side rotates toward the viewer while the front turns away.
If you want a squarer footprint for a catalog, swap aspect-ratio for a perfect square. If you need a refresher on clean square CSS, see the guide on how to make a square with CSS. For avatars or circular logos inside the card, use border-radius: 50% or review how to make a circle with CSS. To add a directional nudge on the front (for example a small arrow cue near the Details button), a tiny shape from how to make an arrow right with CSS blends in without extra images.
Step 4: Building the Flip Interaction
We will support three triggers. Hover for pointer devices, focus-within for keyboard users, and an optional .is-flipped class for a custom toggle. The inner wrapper rotates while the faces stay aligned back-to-back. You can switch to rotateX for a vertical flip if the content calls for it.
/* CSS */
/* Hover on desktops and laptops */
@media (hover: hover) and (pointer: fine) {
.flip-card:hover .flip-card__inner {
transform: rotateY(180deg);
}
}
/* Keyboard focus support: flip while the card or its children hold focus */
.flip-card:focus-within .flip-card__inner,
.flip-card:focus .flip-card__inner {
transform: rotateY(180deg);
}
/* Optional JS toggle: add .is-flipped to the card */
.flip-card.is-flipped .flip-card__inner {
transform: rotateY(180deg);
}
/* Variant: vertical flip (uncomment to try) */
/*
.flip-card:hover .flip-card__inner { transform: rotateX(180deg); }
.flip-card__back { transform: rotateX(180deg); }
*/
How This Works (Code Breakdown)
The transform is applied to .flip-card__inner, not the faces. That keeps the flip consistent and lets both faces share the same transition. We scope the hover rule inside a media query so touch-only devices do not lock the card in a weird state after a single tap. The focus-within rule catches tab navigation into any interactive child, which helps keyboard users reveal the back content without a mouse.
The .is-flipped hook gives you a reliable entry point when you want to flip via a click on the Details button or through an external control. If you prefer a vertical flip, rotateX is the only change you need. RotateX swings the card over a horizontal axis; rotateY swings over a vertical axis. Keep face transforms in sync with the chosen axis.
Advanced Techniques: Adding Animations & Hover Effects
Small motion cues sell the illusion. A subtle parallax tilt, a moving lens flare, or a shadow shift makes the flip feel tactile. Keep these effects gentle so they do not overpower the content.
/* CSS */
/* Parallax tilt while hovering the front face */
@media (hover: hover) and (pointer: fine) {
.flip-card:hover .flip-card__front {
transform: translateZ(1px) rotateZ(-.2deg);
}
}
/* Shine sweep across the image on hover */
@media (hover: hover) and (pointer: fine) {
.flip-card:hover .flip-card__front::after {
opacity: .55;
transition: transform 900ms var(--easing), opacity 300ms ease;
transform: translateX(20%);
}
}
.flip-card__front::after {
transition: transform 1200ms var(--easing), opacity 300ms ease;
transform: translateX(-20%);
}
/* Faster, snappier flip variant */
.flip-card--fast .flip-card__inner {
transition-duration: 500ms;
}
/* Reduced motion: remove animation, keep instant state changes */
@media (prefers-reduced-motion: reduce) {
.flip-card__inner,
.flip-card__front::after {
transition-duration: 0s !important;
animation: none !important;
}
}
The parallax tweak nudges the front face forward a single pixel in 3D space and adds a micro rotation. The shine sweep is a soft light gradient that slides a little on hover, which gives a hint of glass. The reduced motion query keeps state changes functional but removes the animation time; users get the back face immediately when they interact.
Accessibility & Performance
Cards are often dense with text, links, and images, so treat the flip as progressive detail rather than a content gate. The front should stand alone. The back should repeat context in its heading so users know what they are reading after the flip.
Accessibility
Use meaningful alt text for media and a clear heading on both faces. The card uses role=”group” so screen readers treat it like a single unit. The front’s button has an aria-label that explains its action. If you want a persistent toggle, consider adding a small script that toggles .is-flipped and aria-expanded on the button. That gives keyboard and screen reader users a predictable control.
/* CSS */
/* Visual state mapping for a toggling control */
.flip-card[aria-expanded="true"] .flip-card__inner {
transform: rotateY(180deg);
}
When you wire a control, keep focus management simple. Move focus to the back face’s first heading or keep it on the button while the state changes. Both patterns test well. Keep hover-only cues optional and back them up with focus styles, which this demo already includes. The reduced motion media query avoids long flips for users who prefer less animation.
Performance
Transforms and opacity animations run on the compositor, which keeps the frame budget healthy. Avoid animating box-shadow or large filter chains during the flip. Use contain and aspect-ratio to prevent layout churn as images load. If you flip many cards at once, drop the duration to around 400-500ms and dial back extra effects like the shine sweep. Reserve will-change: transform for hotspots only, and remove it when the card is idle to avoid memory pressure.
Ship It With Confidence
You built a 3D flipping card that runs on CSS transforms, flips on hover and focus, and supports a class-based toggle for taps. You also set up motion preferences and learned a few polish tricks that keep the effect crisp. Use this foundation to build product tiles, profile cards, or a full grid of previews that flip to focused details when needed.