翻转卡片
卡片以 3D 方式翻转,露出背面的详细信息;再次点击翻回正面。
用 CSS 3D 为 React 制作翻转卡片。卡片是一个带 aria-pressed 的 <button>,以支持键盘和屏幕阅读器。内部元素设置 transform-style: preserve-3d,并对 transform 设置 600ms、cubic-bezier(0.65, 0, 0.35, 1) 的过渡;按下时旋转 rotateY(180deg)。正反两面用 position absolute 叠放,设置 backface-visibility: hidden,背面预先旋转 rotateY(180deg)。按钮上设置 perspective: 900px。正面显示“咖啡乌”和“点击翻面”;背面显示“加糖的黑咖啡,浓郁香甜,不加奶。”和“再次点击翻回”。用 aria-hidden 对屏幕阅读器隐藏背对用户的那一面。在 prefers-reduced-motion 下改为 150ms 的两面淡入淡出,不做旋转。
源代码
'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; }
}