Flip Card
The card turns over in 3D to reveal details on its back, and turns back on the next click.
Build a React flip card in CSS 3D. The card is a <button> with aria-pressed so it works with keyboard and screen readers. Inside, an inner element has transform-style: preserve-3d and a 600ms transition on transform with cubic-bezier(0.65, 0, 0.35, 1); when pressed it rotates rotateY(180deg). The front and back faces are stacked with position absolute, backface-visibility: hidden, and the back face is pre-rotated rotateY(180deg). Add perspective: 900px on the button. Front shows "Kopi O" with "Tap to reveal"; back shows "Black coffee with sugar. Strong, sweet, no milk." and "Tap again to flip back". Hide the face that is turned away from screen readers with aria-hidden. Under prefers-reduced-motion, crossfade the faces over 150ms instead of rotating.
Source
'use client'
import { useState } from 'react'
import s from './demo.module.css'
export default function FlipCard({ labels }: { labels: Record<string, string> }) {
const [flipped, setFlipped] = useState(false)
return (
<button type="button" className={s.card} aria-pressed={flipped} onClick={() => setFlipped(!flipped)}>
<span className={s.inner}>
<span className={`${s.face} ${s.front}`} aria-hidden={flipped}>
<strong className={s.title}>{labels.title}</strong>
<span className={s.hint}>{labels.front}</span>
</span>
<span className={`${s.face} ${s.back}`} aria-hidden={!flipped}>
<span className={s.text}>{labels.back}</span>
<span className={s.hint}>{labels.hint}</span>
</span>
</span>
</button>
)
}
.card {
width: 200px;
height: 180px;
padding: 0;
border: 0;
background: none;
perspective: 900px;
cursor: pointer;
}
.card:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 4px; border-radius: 16px; }
.inner {
position: relative;
display: block;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 600ms var(--ease-in-out);
}
.card[aria-pressed='true'] .inner { transform: rotateY(180deg); }
.face {
position: absolute;
inset: 0;
display: grid;
align-content: space-between;
padding: 18px;
border-radius: 16px;
text-align: left;
backface-visibility: hidden;
}
.front { background: var(--stage-accent); color: var(--stage-accent-ink); }
.back { background: var(--stage-surface); border: 1px solid var(--stage-rule); color: var(--stage-ink); transform: rotateY(180deg); }
.title { font: 700 var(--text-2xl) / 1.1 var(--font-display); }
.text { font: 400 var(--text-sm) / 1.5 var(--font-body); }
.hint { font: 500 var(--text-xs) / 1.2 var(--font-mono); letter-spacing: 0.04em; opacity: 0.75; }
@media (prefers-reduced-motion: reduce) {
.inner { transition: none; }
.card[aria-pressed='true'] .inner { transform: none; }
.face { transition: opacity 150ms linear; transform: none; }
.card[aria-pressed='true'] .front, .card[aria-pressed='false'] .back { opacity: 0; }
}