CSS 3D flip card with front and back faces
Blog

Build a CSS 3D Flip Card with HTML, CSS and JavaScript

Learn how to create an accessible 3D flip card that reveals a second face when activated. This example uses CSS perspective and transforms with a native button and a small JavaScript handler, so it works with a mouse, touch, and keyboard.

Quick answer: Place the front and back faces inside a 3D-preserving wrapper, hide each face’s reverse side with backface-visibility: hidden, then rotate the wrapper by 180 degrees. Toggle the state with a button instead of relying on hover alone.

View Live Demo Download Source Code

Try the interactive flip card

Activate the demo button to switch between the front and back faces. The control supports click, tap, Enter, and Space. The example runs in your browser and does not send data anywhere.

How the CSS 3D flip works

The card itself establishes perspective, which gives the rotation a sense of depth. An inner wrapper uses transform-style: preserve-3d to keep both faces in the same 3D space. The back face starts rotated 180 degrees, and backface-visibility: hidden keeps reversed text from showing through. JavaScript toggles a class on the wrapper to flip the card.

Build the flip card

1. Add the HTML structure

Use a button as the interactive control. The aria-pressed attribute communicates the current state, while aria-hidden keeps the inactive face out of the accessibility tree.

2. Add the CSS 3D transforms

Put perspective on the outer card and preserve the 3D space on its inner wrapper. Position both faces in the same frame, then rotate the back face before the wrapper turns.

3. Toggle the card with JavaScript

The native button already supports keyboard activation. The click handler updates the visual class, pressed state, accessible face, and a short live status message.

Customize the animation

  • Change rotateY(180deg) to rotateX(180deg) for a vertical flip.
  • Adjust the perspective value to make the depth stronger or softer.
  • Replace the sample text and colors while keeping both faces the same size.
  • Use a reduced-motion media query to disable the transition for visitors who request less motion.

Accessibility and responsive behavior

A button makes the interaction available to keyboard and touch users without hover. Keep a visible focus style, maintain readable color contrast on both sides, and respect the operating system’s reduced-motion setting. Use a fluid width so the card fits narrow screens.

Common problems

The back face looks mirrored

Rotate the back face by 180 degrees and hide the reverse sides of both faces using the standard and WebKit-prefixed backface visibility properties.

The card looks flat

Apply perspective to the outer card and transform-style: preserve-3d to the inner wrapper. Keep both faces positioned in the same card frame.

The animation only works with a mouse

Do not use hover as the only trigger. A button-controlled class works with clicks, touch, Enter, and Space.

Frequently asked questions

Can I create a flip card with CSS only?

CSS can animate the card on hover or focus. A small JavaScript state toggle makes the example work consistently with touch and lets the button announce its current state.

Does this example require a JavaScript library?

No. It uses standard CSS transforms and the browser’s built-in event listener API.

Does the example support reduced motion?

Yes. The CSS disables the transition when the visitor’s device requests reduced motion.

Conclusion

A CSS 3D flip card combines two faces in a compact component. Perspective, preserved 3D transforms, and a button-controlled state make the effect responsive and accessible. Try the demo, then download the files to adapt the card to your project.

HTML structure
<button class="flip-card" type="button" aria-pressed="false" aria-label="Flip example card">
  <span class="flip-card__inner">
    <span class="flip-card__face flip-card__front" aria-hidden="false">
      <span class="card-kicker">FRONT SIDE</span>
      <span class="card-title">Build something memorable.</span>
    </span>
    <span class="flip-card__face flip-card__back" aria-hidden="true">
      <span class="card-kicker">BACK SIDE</span>
      <span class="card-title">A little depth goes a long way.</span>
    </span>
  </span>
</button>
<p class="demo-status" aria-live="polite">Card is showing the front.</p>
console.log('Hello World');
CSS 3D flip card styles
.flip-card { width: min(100%, 390px); height: 280px; padding: 0; border: 0; border-radius: 24px; background: transparent; perspective: 1200px; cursor: pointer; }
.flip-card:focus-visible { outline: 4px solid #ff9a76; outline-offset: 6px; }
.flip-card__inner { position: relative; display: block; width: 100%; height: 100%; border-radius: inherit; transform-style: preserve-3d; transition: transform 650ms cubic-bezier(.2, .75, .25, 1); }
.flip-card.is-flipped .flip-card__inner { transform: rotateY(180deg); }
.flip-card__face { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: center; gap: .75rem; padding: 2rem; border-radius: inherit; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.flip-card__front { color: #fff; background: #2459ad; }
.flip-card__back { color: #fff; background: #087a88; transform: rotateY(180deg); }
.card-kicker { font-size: .8rem; letter-spacing: .12em; font-weight: 700; }
.card-title { font-size: clamp(1.5rem, 4vw, 2.2rem); line-height: 1.15; font-weight: 700; }
.demo-status { margin-top: .75rem; }
@media (prefers-reduced-motion: reduce) { .flip-card__inner { transition: none; } }
console.log('Hello World');
JavaScript flip interaction
const card = document.querySelector(".flip-card");
const front = card.querySelector(".flip-card__front");
const back = card.querySelector(".flip-card__back");
const status = document.querySelector(".demo-status");

card.addEventListener("click", () => {
  const isFlipped = card.classList.toggle("is-flipped");
  card.setAttribute("aria-pressed", String(isFlipped));
  front.setAttribute("aria-hidden", String(isFlipped));
  back.setAttribute("aria-hidden", String(!isFlipped));
  status.textContent = isFlipped
    ? "Card is showing the back."
    : "Card is showing the front.";
});
console.log('Hello World');

Card is showing the front.