普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月2日首页

理解 CSS backface-visibility:卡片翻转效果背后的属性

作者 parade岁月
2025年12月2日 13:16

前段时间在做一个产品展示页,需要实现卡片翻转效果。本以为很简单,结果翻转的时候总是"穿帮"——正面和背面同时显示,或者背面的文字是镜像的。折腾了半天才发现,原来是 backface-visibility 这个属性没搞明白。

今天就来聊聊这个 CSS 属性。

🎮 在线演示frontend-learning.pages.dev/animation/d…

建议边看文章边打开演示页面,所有效果都可以直接交互体验。

先说结论

backface-visibility 控制的是:当一个元素的背面朝向你时,这个背面是否可见

听起来有点绕?没关系,我们慢慢来。

理解"背面"这个概念

这是理解整个属性的关键。很多人(包括我)一开始都没搞清楚这个"背面"到底是什么。

关键理解:每个 HTML 元素就像一张纸,天生就有正面和背面。当你用 rotateY(180deg) 翻转它时,你看到的是它的背面(就像纸翻过来,文字是镜像的)。

来看个最简单的例子:

<div
  style="
  background: linear-gradient(135deg, #10b981, #059669);
  color: white;
  padding: 40px;
  transform: rotateY(180deg);
"
>
  HELLO
</div>

你会看到什么?镜像的 HELLO。这就是元素的背面。

backface-visibility: hidden 的作用就是:当背面朝向你时,让它变透明

四个场景,逐步理解

我做了四个对比演示,帮你理解这个属性是怎么工作的。

场景 1:正常叠加

<div style="position: relative;">
  <div class="front" style="position: absolute;">FRONT</div>
  <div class="back" style="position: absolute;">BACK</div>
</div>

结果:只看到 BACK 原因:两个都是 absolute,BACK 在上层覆盖了 FRONT

这个很好理解,就是普通的 DOM 层叠。

场景 2:BACK 翻转 180°

.back {
  transform: rotateY(180deg);
}

结果:看到镜像的 BACK 原因:BACK 翻转后,你看到的是它的背面(文字镜像)

这里就出现问题了!虽然 BACK 翻转了,但你看到的是它的背面,文字是反的。

场景 3:隐藏 BACK 的背面

.back {
  transform: rotateY(180deg);
  backface-visibility: hidden; /* 关键!*/
}

结果:✅ 只看到 FRONT 原因:BACK 的背面被隐藏,露出下面的 FRONT

现在好了!BACK 的背面不可见了,所以你能看到下面的 FRONT。

场景 4:完美翻转

.front,
.back {
  backface-visibility: hidden; /* 两面都隐藏背面 */
}
.back {
  transform: rotateY(180deg);
}

/* 悬停时翻转父容器 */
.card-container:hover .card {
  transform: rotateY(180deg);
}

结果:✅ 悬停完美翻转 原理

  • 初始状态:FRONT 正面朝向你(显示),BACK 背面朝向你(隐藏)
  • 翻转后:FRONT 背面朝向你(隐藏),BACK 正面朝向你(显示)

这就是完美的卡片翻转效果!

语法很简单

.element {
  backface-visibility: visible | hidden;
}
  • visible:默认值,背面可见
  • hidden:背面不可见

就这两个值,没别的了。

一个常见误区:为什么不能分别旋转子元素?

这是我当时最困惑的地方。既然翻转后正面和背面都旋转了 180°,为什么不能直接这样写?

/* ❌ 错误写法 */
.card-container:hover .front {
  transform: rotateY(180deg);
}
.card-container:hover .back {
  transform: rotateY(180deg);
}

看起来很合理对吧?但实际上完全不行

问题在哪?

关键在于:CSS 的 transform 属性会被完全覆盖,而不是累加

/* 初始状态 */
.front {
  transform: rotateY(0deg);
}
.back {
  transform: rotateY(180deg);
}

/* 悬停后 */
.card-container:hover .front {
  transform: rotateY(180deg); /* ✓ 从 0° 变成 180° */
}
.card-container:hover .back {
  transform: rotateY(180deg); /* ✗ 从 180° 变成 180°,没变化! */
}

背面的初始值 rotateY(180deg) 被新值 rotateY(180deg) 覆盖,但因为值相同,所以背面根本没动!

正确做法:旋转父容器

/* ✅ 正确写法 */
.card-container:hover .card {
  transform: rotateY(180deg); /* 旋转整个父容器 */
}

这样做的原理是:子元素的 transform 是相对于父元素的坐标系

初始状态:
.card (0°)
  ├── .front: rotateY(0°) 相对于 .card   → 绝对位置 0°
  └── .back:  rotateY(180°) 相对于 .card → 绝对位置 180°

悬停后:
.card (180°)  ← 整个坐标系旋转了
  ├── .front: rotateY(0°) 相对于 .card   → 绝对位置 0° + 180° = 180°
  └── .back:  rotateY(180°) 相对于 .card → 绝对位置 180° + 180° = 360° = 0°

父元素旋转时,子元素的相对角度不变,但绝对角度会改变。这才是正确的翻转逻辑。

为什么需要三层结构?

标准的卡片翻转需要三层 DOM 结构:

<div class="爷容器">
  <!-- perspective(观察点) -->
  <div class="父容器">
    <!-- transform-style + rotateY(翻转者) -->
    <div class="正面"></div>
    <!-- backface-visibility(被翻转的面) -->
    <div class="背面"></div>
    <!-- backface-visibility(被翻转的面) -->
  </div>
</div>

每一层的职责

爷容器:设置 perspective

.card-container {
  perspective: 1000px; /* 必须在父元素上 */
}
  • 定义观察者的位置,创建 3D 空间
  • 类比:你站在舞台前看表演,这个属性决定你站在哪里看
  • 为什么在外层perspective 必须设置在父元素上,才能对子元素的 3D 变换生效

父容器:设置 transform-style 和执行 rotateY

.card {
  transform-style: preserve-3d; /* 保持 3D 空间 */
  transition: transform 0.6s;
}

.card-container:hover .card {
  transform: rotateY(180deg); /* 翻转整个卡片 */
}
  • preserve-3d:让子元素保持在 3D 空间中(而不是被压平)
  • 类比:这是舞台上的转盘,带着上面的演员一起旋转

子元素:正面和背面

.card-front,
.card-back {
  position: absolute;
  backface-visibility: hidden; /* 关键!隐藏背面 */
}

.card-back {
  transform: rotateY(180deg); /* 背面预先翻转 */
}
  • position: absolute:让两个面重叠在同一位置
  • 背面预先翻转 180°:这样当父容器翻转 180° 时,背面刚好正面朝向你

为什么不能少一层?

如果只有两层:

/* ❌ 错误 */
.card {
  perspective: 1000px;
  transform: rotateY(180deg);
}

问题perspectivetransform 在同一个元素上,perspective 不会对自己的 transform 生效,只会对子元素生效。结果就是没有透视效果。

完整的卡片翻转模板

直接复制这个模板,改改样式就能用:

<!-- HTML 结构 -->
<div class="card-container">
  <!-- 爷容器:观察点 -->
  <div class="card">
    <!-- 父容器:翻转者 -->
    <div class="card-front">正面内容</div>
    <div class="card-back">背面内容</div>
  </div>
</div>
/* 第 1 层:爷容器 - 设置观察点 */
.card-container {
  perspective: 1000px; /* 必须在父元素上 */
}

/* 第 2 层:父容器 - 执行翻转 */
.card {
  transform-style: preserve-3d; /* 保持 3D 空间 */
  transition: transform 0.6s;
}

.card-container:hover .card {
  transform: rotateY(180deg); /* 翻转整个卡片 */
}

/* 第 3 层:子元素 - 正面和背面 */
.card-front,
.card-back {
  position: absolute;
  width: 100%;
  height: 100%;
  backface-visibility: hidden; /* 关键!隐藏背面 */
}

.card-back {
  transform: rotateY(180deg); /* 背面预先翻转 */
}

这个模板是标准写法,三层结构缺一不可。

几个注意事项

  1. 必须配合 3D 变换backface-visibility 只在 3D 变换(如 rotateY, rotateX)中有意义,2D 旋转(rotate)不会产生背面
  2. 性能优化:设置为 hidden 可以让浏览器跳过背面的渲染,提升性能
  3. 浏览器兼容:现代浏览器都支持,旧版本可能需要 -webkit- 前缀

总结

backface-visibility 这个属性本身很简单,就两个值。但要真正理解它,需要搞清楚几个概念:

  1. 每个元素天生就有正面和背面
  2. backface-visibility: hidden 让背面朝向你时变透明
  3. 卡片翻转必须在父元素上执行旋转
  4. 标准的三层结构不能省略

最后再推荐一次我做的演示页面,里面有所有场景的交互演示和详细说明:

👉 frontend-learning.pages.dev/animation/d…

❌
❌