在 CSS 的诸多能力里,3D 变换是最容易让人“一眼惊艳”、也最容易让人“一眼翻车”的一项。几行 transform 就能让平面元素立起来、转起来、叠出空间纵深;但同样几行代码,也可能让文字糊成一团、让 z-index 彻底失效、让页面在低端手机上抖成幻灯片。3D 不是“加上去”的效果,而是一整套空间模型。本篇 40 分钟深度长文,从坐标系与透视投影讲起,穿过 perspective、transform-style、backface-visibility,一路走到立方体、翻转卡片、鼠标跟随倾斜、分层视差、3D 环形轮播与性能调优,中间穿插多个可亲手拖拽、亲手调参的交互实验台,帮你把 3D 变换从“抄代码能用”升级到“闭着眼睛也敢写”。
先建立空间感:CSS 的 3D 坐标系
在 2D 的世界里,我们只关心两个方向:X 向右为正,Y 向下为正。而一旦进入 3D,就多出了第三个轴:Z。CSS 的 Z 轴方向很关键——正方向指向观察者(朝屏幕外),负方向指向屏幕深处。
这个细节非常重要,因为它决定了 translateZ(100px) 到底是“往前推”还是“往后退”。在 CSS 中,translateZ(100px) 表示元素向观察者靠近 100px,视觉上会变大;translateZ(-100px) 则表示元素远离观察者,视觉上会缩小。
/* X 轴:水平方向,右为正 */
transform: translateX(100px);
/* Y 轴:垂直方向,下为正(注意与数学坐标相反) */
transform: translateY(100px);
/* Z 轴:垂直屏幕,朝观察者为正 */
transform: translateZ(100px); /* 靠近,变大 */
transform: translateZ(-100px); /* 远离,变小 */
/* 三合一简写 */
transform: translate3d(40px, 20px, 80px);
/* 绕轴旋转(右手法则) */
transform: rotateX(45deg); /* 绕水平轴翻转,像点头 */
transform: rotateY(45deg); /* 绕垂直轴旋转,像摇头 */
transform: rotateZ(45deg); /* 绕屏幕轴旋转,等于 2D rotate */
下面这个坐标系可以按住拖动,亲手转一转,建立直觉。红色是 X 轴,绿色是 Y 轴,蓝色是 Z 轴,底部网格是 z = 0 平面。
按住鼠标(或手指)拖动,从不同角度观察三个坐标轴
为什么必须有透视?
如果你只写 transform: rotateY(45deg),元素看起来只会“变窄”,而不是“转过去”。因为浏览器默认使用的是正交投影——远处的物体和近处的物体一样大,没有近大远小的感觉。
只有加上 perspective,浏览器才会切换到透视投影,让元素呈现出符合人眼习惯的纵深感。这是 3D 与 2D 之间最关键的一道分水岭。
perspective:为 3D 场景架起一台相机
perspective 定义的是观察者眼睛到 z = 0 平面之间的距离,单位是像素。它决定了 3D 场景的“畸变强度”,作用相当于摄影里的焦距:
- 数值越小(如 200px):相机离得近,透视感强烈,畸变大,画面有冲击力,但容易变形。
- 数值越大(如 2000px):相机离得远,透视感微弱,画面接近正交投影,更“平”。
- 常用区间:卡片类 600~1000px,立方体 800~1200px,整页场景 1200~2000px。
.scene {
perspective: 800px;
perspective-origin: 50% 50%;
}
.card {
transform: rotateY(45deg);
}
/* 写法二:把 perspective() 写进 transform(只影响自身) */
.card {
transform: perspective(800px) rotateY(45deg);
}
/* 两者的区别非常大: */
/* 父级 perspective → 多个子元素共享同一个消失点,*/
/* 像同一个场景里的多个物体,空间关系正确。 */
/* transform: perspective() → 每个元素各有一个相机,*/
/* 各自独立,做立方体时会“面飞出去”。 */
动手实验:拖动滑块改变 perspective
下面这张卡片固定旋转 rotateY(45deg),唯一变化的是 perspective 的值。仔细体会:数值越小时,卡片的左右边缘被拉伸得越夸张;数值越大时,卡片越接近一个被压扁的平面矩形。
perspective-origin 决定的是“消失点”在舞台上的位置。改成 left 之后,你会看到卡片朝着左前方“收缩”,就像你站在它的左侧看它。这是做出“视角跟随”效果的基础。
一个容易踩的坑:perspective 会创建包含块
给元素加上 perspective 之后,它会成为后代 position: fixed 元素的包含块。也就是说,如果舞台上有一个固定定位的元素,它会被限制在舞台内部,而不是相对视口定位。做全屏 3D 场景时,这一点务必留意。
transform 函数全景:3D 工具箱
CSS 提供的 3D 变换函数并不多,但组合起来威力巨大。下面这张表可以当作速查手册。
| 函数 | 作用 | 参数 | 典型用途 |
|---|---|---|---|
translate3d(x, y, z) |
三轴位移 | 长度值 | 空间中的位置移动 |
translateZ(z) |
沿 Z 轴位移 | 长度值 | 分层、景深、视差 |
rotate3d(x, y, z, a) |
绕任意向量旋转 | 向量 + 角度 | 斜轴旋转、自定义转轴 |
rotateX(a) |
绕水平轴旋转 | 角度 | 上下翻转、卡片俯仰 |
rotateY(a) |
绕垂直轴旋转 | 角度 | 左右翻转、环形排布 |
rotateZ(a) |
绕屏幕轴旋转 | 角度 | 等价 2D 旋转 |
scale3d(sx, sy, sz) |
三轴缩放 | 数字 | 整体缩放(sz 影响很小) |
matrix3d(…) |
4×4 变换矩阵 | 16 个数字 | JS 计算、复杂组合、动画插值 |
顺序决定一切
多个变换函数是从右往左依次应用的,等价于矩阵连乘。顺序不同,结果完全不同:
transform: rotateY(45deg) translateZ(100px);
/* B:先旋转,再位移 —— 位移方向也跟着旋转了 */
transform: translateZ(100px) rotateY(45deg);
/* 两者视觉结果完全不同: */
/* A 的结果是元素沿旋转后的 Z 轴方向移动,会跑到侧面;*/
/* B 的结果是元素先平移再自转,位置更可控。*/
/* 排列卡片环时的经典写法: */
transform: rotateY(90deg) translateZ(240px);
/* 先转到对应角度,再沿旋转后的 Z 轴推出去 —— 正好围成圆环 */
动手实验:3D 变换调参台
下面把 rotateX、rotateY、rotateZ、translateZ、scale 与 perspective 全部暴露成滑块。拖动任意一项,方块立刻响应,下方同步生成对应的 CSS 代码。
观察要点:
- 把
rotateX调到 ±90° 附近,方块的“厚度”会消失,因为它被压成了一条线; translateZ调大时方块会变大——这并不是scale,而是真的“靠近了相机”;perspective很小时,translateZ的变化会被极度放大,甚至让元素“撞到镜头”消失;- 旋转顺序固定为 X → Y → Z,所以调参数值时要注意顺序带来的差异。
transform-style:让子元素留在同一个空间里
这是 3D 变换里最容易被忽略、也最关键的一个属性。默认情况下,一个元素的 transform-style 是 flat——意思是:它的所有子元素最终都会被压扁到它自己的平面上。无论子元素写了多少 translateZ,看起来都只是简单的缩放。
只有把父元素设为 transform-style: preserve-3d,子元素才能保留自己在 3D 空间中的位置,真正实现“面与面立起来组成一个立方体”。
.cube {
transform-style: flat;
}
/* ✅ preserve-3d:子元素保留在 3D 空间 */
.cube {
transform-style: preserve-3d;
}
/* 完整的三层结构: */
/* 第一层:提供透视 */
.scene {
perspective: 900px;
}
/* 第二层:保留 3D 空间并整体旋转 */
.cube {
transform-style: preserve-3d;
transform: rotateX(-20deg) rotateY(30deg);
}
/* 第三层:六个面各自定位 */
.face {
position: absolute;
inset: 0;
}
.face-front { transform: translateZ(60px); }
.face-right { transform: rotateY(90deg) translateZ(60px); }
.face-back { transform: rotateY(180deg) translateZ(60px); }
.face-left { transform: rotateY(-90deg) translateZ(60px); }
.face-top { transform: rotateX(90deg) translateZ(60px); }
.face-bottom { transform: rotateX(-90deg) translateZ(60px); }
动手实验:切换 preserve-3d
下面有三张卡片,分别位于 translateZ(-70px)、0、70px,父元素整体旋转了 42 度。点击按钮切换 transform-style,观察它们的空间关系如何瞬间“塌陷”。
你会清楚地看到:preserve-3d 时三张卡片在空间中前后错开,形成真实的纵深;切换成 flat 后,它们被压回同一个平面,只剩下大小差异。
破坏 preserve-3d 的“隐形杀手”
即使你写了 preserve-3d,某些属性也会强制把它“打回” flat,这是很多人百思不得其解的问题:
overflow为hidden、auto、scroll时;opacity小于 1;filter不为none;clip-path不为none;mask/mask-image不为none;mix-blend-mode不为normal;isolation: isolate;contain: paint;will-change声明了上述某些属性。
.cube { transform-style: preserve-3d; overflow: hidden; }加了
overflow: hidden 之后,3D 立刻失效,六个面被压平重叠在一起。
.scene { overflow: hidden; }把裁剪放到更外层的场景容器上,
preserve-3d 的层级保持干净。
backface-visibility:背面要不要露出来
每个元素都有正反两面。默认情况下,当元素旋转超过 90 度,你看到的其实是它背面的镜像。backface-visibility: hidden 可以让背面完全不渲染。
这个属性是做翻转卡片的核心:正面和背面两张卡片叠在一起,背面那张预先旋转 180 度。不翻转时正面可见、背面被隐藏;翻转后正好相反。
.flip-scene {
perspective: 1000px;
}
.flip-inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.7s cubic-bezier(0.34, 1.3, 0.64, 1);
}
.flip-scene.flipped .flip-inner {
transform: rotateY(180deg);
}
.flip-face {
position: absolute;
inset: 0;
backface-visibility: hidden;
}
.flip-face.back {
transform: rotateY(180deg);
}
动手实验:点击翻转
点击卡片翻转,再点一次翻回来
注意翻转的缓动:这里用了 cubic-bezier(0.34, 1.3, 0.64, 1),第二个控制点大于 1,所以卡片在翻到位之前会轻微“过冲”一下,手感更像真实物体。
实战一:纯 CSS 3D 立方体
立方体是检验 3D 理解是否到位的试金石。它需要你把前面所有知识串起来:透视、preserve-3d、六个面的旋转与位移。
核心思路
- 场景容器设置
perspective; - 立方体容器设置
transform-style: preserve-3d,尺寸为 120×120; - 六个面绝对定位铺满容器,各自先旋转到朝向正确方向,再沿旋转后的 Z 轴推出半个边长(60px);
- 立方体容器整体旋转,就能看到立体效果。
/* 前面:不需要旋转,直接往前推 */
.f-front { transform: translateZ(60px); }
/* 后面:先转 180° 让它背对镜头,再推出去 */
.f-back { transform: rotateY(180deg) translateZ(60px); }
/* 右面:先转 90°,原来的“右侧”就朝向了前方 */
.f-right { transform: rotateY(90deg) translateZ(60px); }
/* 左面:反方向转 90° */
.f-left { transform: rotateY(-90deg) translateZ(60px); }
/* 上面:绕 X 轴转 90° */
.f-top { transform: rotateX(90deg) translateZ(60px); }
/* 下面:绕 X 轴反方向转 90° */
.f-bottom { transform: rotateX(-90deg) translateZ(60px); }
/* 自动旋转 */
@keyframes spin {
from { transform: rotateX(-20deg) rotateY(0deg); }
to { transform: rotateX(-20deg) rotateY(360deg); }
}
.cube { animation: spin 12s linear infinite; }
下面这个立方体由 JS 驱动,按住拖动可以自由旋转,松手后会继续自动缓慢旋转。
按住拖动旋转立方体,松手后自动恢复旋转
立方体的三个易错点
- 忘记给立方体容器设 preserve-3d:六个面会全部重叠在同一个平面上。
- 用了
transform: perspective()而不是父级 perspective:每个面各自透视,立方体会“炸开”。 - 面的尺寸与
translateZ不匹配:半边长应该是边长的一半,否则会露出缝隙或互相穿插。
实战二:鼠标跟随的 3D 倾斜卡片
这是现代落地页里最常见的 3D 效果之一:鼠标移到卡片上,卡片会根据指针位置朝不同方向轻微倾斜,仿佛能“摸到”它。
数学原理
假设鼠标在卡片内的相对坐标为 (px, py),取值范围 0~1:
rotateY与水平位置相关:(px - 0.5) * 2 * maxAnglerotateX与垂直位置相反相关:-(py - 0.5) * 2 * maxAngle- 最大倾角一般取 8°~15°,超过 20° 就会显得夸张且容易露出背面。
card.addEventListener('pointermove', (e) => {
const rect = card.getBoundingClientRect();
const px = (e.clientX - rect.left) / rect.width; /* 0 ~ 1 */
const py = (e.clientY - rect.top) / rect.height; /* 0 ~ 1 */
const max = 12;
const ry = (px - 0.5) * 2 * max;
const rx = -(py - 0.5) * 2 * max;
card.style.transform =
`rotateX(${rx}deg) rotateY(${ry}deg)`;
});
/* 离开时复位 */
card.addEventListener('pointerleave', () => {
card.style.transform = 'rotateX(0) rotateY(0)';
});
/* CSS:内容层沿 Z 轴抬起,制造悬浮感 */
.tilt-inner {
transform: translateZ(46px);
}
动手实验:把鼠标移到卡片上
3D Tilt Card
鼠标在卡片上移动,感受倾角与高光
三个让效果更高级的细节
- 内容层抬高:给内部文字加
translateZ(40~60px),倾斜时文字会有明显的悬浮感,而不是贴在卡片上。 - 高光跟随:用
radial-gradient加--mx / --my变量,让高光跟着指针移动,金属质感立现。 - 过渡要分两段:移动时
transition: transform 0.1s保证跟手,离开时改成0.4s让它缓缓复位。
性能上,这个效果只改 transform,全程走合成,几乎不占主线程。唯一需要注意的是:不要在 pointermove 里读取会引起重排的属性(如 offsetWidth),必要时用 getBoundingClientRect() 并缓存结果。
实战三:分层视差与景深
把不同元素放在不同的 translateZ 上,再整体轻微旋转,就能得到天然的视差效果——近处的层移动快,远处的层移动慢。这是“景深”的视觉本质。
.parallax-space {
transform-style: preserve-3d;
transform: rotateX(var(--rx)) rotateY(var(--ry));
}
.p1 { transform: translate(-50%, -50%) translateZ(-90px); }
.p2 { transform: translate(-50%, -50%) translateZ(0px); }
.p3 { transform: translate(-50%, -50%) translateZ(90px); }
.p4 { transform: translate(-50%, -50%) translateZ(180px); }
/* JS:鼠标位置映射到 ±10° 的旋转范围 */
scene.addEventListener('pointermove', (e) => {
const r = scene.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width - 0.5;
const y = (e.clientY - r.top) / r.height - 0.5;
space.style.transform =
`rotateX(${-y * 12}deg) rotateY(${x * 16}deg)`;
});
/* 用 CSS 变量更优雅 */
space.style.setProperty('--ry', (x * 16) + 'deg');
动手实验:把鼠标移到下面的场景里
分层视差的关键在于克制:Z 值跨度不要太大(一般不超过 ±200px),旋转角度不要超过 ±15°。过度的视差会让用户产生眩晕感,尤其在移动端。
另外注意:移动端没有鼠标,这类效果应该退化为陀螺仪驱动或干脆关闭。这也是后面要讲的 prefers-reduced-motion 的适用范围之一。
实战四:3D 环形轮播
环形排布的核心只有一行公式:先 rotateY 到对应角度,再沿旋转后的 Z 轴推出去。N 个元素均匀分布时,每个元素的角度是 360 / N 度。
.carousel {
transform-style: preserve-3d;
animation: ringSpin 26s linear infinite;
}
.carousel-item {
position: absolute;
inset: 0;
/* --i 由 JS 或内联样式提供:0, 1, 2, ... 7 */
transform: rotateY(calc(var(--i) * 45deg)) translateZ(240px);
backface-visibility: hidden;
}
@keyframes ringSpin {
from { transform: rotateY(0deg); }
to { transform: rotateY(360deg); }
}
/* 半径公式:为了不重叠,translateZ 的距离应满足 */
/* 2 × R × sin(180° / N) ≥ 卡片宽度 */
/* N = 8、宽 140px 时,R ≥ 183px,取 240px 比较宽松 */
动手实验:把鼠标移到轮播上暂停
让轮播更实用的几个改进
- 鼠标悬停暂停:
.carousel:hover { animation-play-state: paused; },方便用户看清内容。 - 手动切换:用 JS 累加角度而非依赖 CSS 动画,这样可以实现“上一张 / 下一张”。
- 控制背面渲染:加上
backface-visibility: hidden,转过去的卡片不再绘制,节省开销。 - 降低元素数量:8 个已经不少了,12 个以上在移动端就要谨慎。
3D 变换的性能真相
好消息是:transform 相关的属性(包括所有 3D 变换)只触发合成,不触发布局与绘制,这是它最大的性能优势。坏消息是:3D 变换往往会创建合成层,而合成层是要吃显存的。
Style → Composite
/* 跳过 Layout 与 Paint,这是巨大的优势 */
/* 但代价是: */
/* 1. 元素会被提升为合成层 */
/* 2. 图层需要 GPU 显存 */
/* 3. 图层数量过多时,合成本身也会变慢 */
/* 显存粗略估算:宽 × 高 × 4 字节 */
/* 300 × 200 的卡片 ≈ 240 KB */
/* 100 张这样的卡片 ≈ 24 MB —— 还可接受 */
/* 但如果每张都 1200 × 800,就会直接爆掉 */
/* ✅ 推荐:只对真正参与 3D 的元素开启 */
.cube, .flip-inner, .tilt-card {
transform-style: preserve-3d;
will-change: transform;
}
/* ✅ 动画结束后释放 */
el.addEventListener('transitionend', () => {
el.style.willChange = 'auto';
});
/* ❌ 危险:给长列表的每一项都加 3D 变换 */
.list-item { transform: translateZ(0); } /* × 500 项 */
不触发 Layout / Paint,主线程负担极小
图层越大越多,GPU 压力越高
只给真正 3D 的元素用
查看图层数量与内存占用
动手实验:布局属性 vs 3D 合成变换
下面两条跑道同时播放“从左到右”的动画。上面那条动画 left(触发布局),下面那条动画 transform: translate3d + rotateY(只走合成)。点击开始,观察右上角的实时帧率。
关于 will-change 与 3D
很多教程会建议“做 3D 动画前加 will-change: transform”。这在大多数情况下是对的,但有三个前提:
- 只加在真正会动的元素上,不要加在容器或通配上;
- 动画开始前加,结束后移除,避免长期占用显存;
- 不要同时声明多个属性,
will-change: transform, opacity, filter会让浏览器为每种可能性都预留资源。
另一个更现代的替代方案是 contain: layout paint:它不创建合成层,但能把布局与绘制的范围锁死在元素内部,对列表项这类重复组件收益明显。
3D 变换的六个经典陷阱
陷阱一:文字模糊
3D 变换后的文字会被 GPU 当作纹理处理,如果变换过程中像素没有对齐,就会出现发虚。常见解法:
.card {
backface-visibility: hidden;
transform: translateZ(0);
-webkit-font-smoothing: antialiased;
}
/* 尽量让旋转角度落在整数度上,减少采样偏移 */
/* 如果文字很小,考虑用 SVG 或位图替代 */
陷阱二:z-index 失效
开启 3D 变换后,元素的层叠顺序不再由 z-index 决定,而是由它在 3D 空间中的 Z 位置决定。距离观察者更近的元素永远在上面。这既是能力也是坑:原本靠 z-index 堆叠的下拉菜单、提示框,可能因为 Z 位置不同而跑到后面去。
陷阱三:父级 overflow 裁掉了 3D 元素
Z 值为正的元素在视觉上会“变大”,可能超出容器边界。如果父容器有 overflow: hidden,超出部分会被裁掉——更糟的是,它还会破坏 preserve-3d。建议把裁剪放在更外层,或者干脆不用裁剪。
陷阱四:闪烁与抖动
在部分 Android 浏览器上,3D 元素在动画过程中会出现闪烁。通常是因为两个面在同一个 Z 平面上产生了深度冲突(Z-fighting)。解法:
- 让相邻面之间有明确的 Z 间距,不要完全重合;
- 给元素加上
backface-visibility: hidden; - 避免在动画过程中改变
transform-style。
陷阱五:3D 变换创建了新的包含块
transform 不为 none 的元素会成为后代 position: fixed 的包含块。这意味着弹窗、悬浮按钮如果被放在 3D 舞台内部,它们的固定定位会失效。正确做法是把这类元素移到舞台外面。
陷阱六:移动端过度使用
移动端 GPU 性能差异极大,且设备发热会触发降频。中低端机型上,多个大尺寸合成层同时动画,很容易掉到 30fps 以下。建议:
- 移动端减少同时进行 3D 动画的元素数量;
- 降低图层尺寸,避免全屏大图层;
- 用媒体查询针对小屏降级为 2D 变换。
.card:hover { transform: rotateY(60deg) scale(1.3); filter: blur(2px); }旋转、缩放、滤镜同时上,还会破坏
preserve-3d,在低端机上必卡。
.card:hover { transform: rotateY(8deg); }角度克制、只动
transform,既有效果又有性能。
用 DevTools 调试 3D 场景
3D 效果光看代码很难判断问题出在哪一层。Chrome DevTools 提供了一套非常直观的 3D 调试工具。
/* Step 1:打开 Elements 面板的 3D 视图 */
/* 点击工具栏中的立方体图标,进入 3D View */
/* 可以旋转整个 DOM 树,直观看到图层堆叠关系 */
/* Step 2:用 Composited Layers 查看分层 */
/* 每个合成层都会显示为一张独立的“卡片” */
/* 如果层数异常多,说明需要减少 3D 元素数量 */
/* Step 3:Layers 面板查看内存 */
/* 每个图层都标注了内存占用,注意总量 */
/* Step 4:Rendering 面板辅助 */
/* ✅ Paint flashing —— 检查是否有意外重绘 */
/* ✅ Layer borders —— 显示图层边界 */
/* ✅ FPS meter —— 实时帧率 */
/* Step 5:Performance 录制 */
/* 观察合成线程的耗时,确认是否落在预算内 */
常见症状与诊断表
- 3D 效果完全看不出来 → 检查父级有没有
perspective,中间层有没有preserve-3d。 - 立方体“炸开”成六个飞散的方块 → 用了
transform: perspective()而不是父级透视。 - 层级关系不对 → 检查是否有
overflow: hidden、filter、opacity < 1破坏了 3D 空间。 - 动画卡顿 → 打开 Layers 面板看图层数量,超过 30 个就要警惕。
- 低端机闪退 → 显存不足,减少大尺寸合成层。
无障碍:3D 效果也需要“安全出口”
3D 效果往往伴随着大幅度位移、旋转与缩放,这对前庭功能障碍、眩晕症用户非常不友好。操作系统提供“减少动态效果”开关,CSS 通过 prefers-reduced-motion 读取它。
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* 更优雅:保留淡入淡出,去掉旋转与位移 */
@media (prefers-reduced-motion: reduce) {
.cube, .carousel, .tilt-card {
animation: none !important;
transform: none !important;
}
.flip-inner {
/* 翻转改为淡入淡出,不做旋转 */
transition: opacity 0.2s linear;
}
}
/* JS 侧同步读取偏好 */
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
if (mq.matches) {
/* 跳过鼠标跟随倾斜、视差等 JS 驱动效果 */
skipTiltEffect();
}
除了动效偏好,3D 效果还要注意信息可访问性:如果某段内容只在立方体的某个面上,屏幕阅读器用户可能无法感知。关键信息应该始终在 DOM 中以可读形式存在,3D 只是它的视觉呈现方式。
兼容性与渐进增强
3D 变换的基础能力支持度非常好,但一些新特性仍需注意退化。
/* transform: translate3d / rotate3d —— 全平台 */
/* perspective / perspective-origin —— 全平台 */
/* transform-style: preserve-3d —— 全平台 */
/* backface-visibility —— 全平台 */
/* transform-box —— 现代浏览器 */
/* contain / content-visibility —— 较新 */
/* @supports 特性检测 —— 全平台 */
/* 渐进增强:先保证 2D 可用,再叠加 3D */
.card {
/* 基础状态:2D 变换,所有浏览器都正常 */
transition: transform 0.35s ease;
}
.card:hover {
transform: translateY(-6px);
}
/* 支持 3D 时才升级为立体效果 */
@supports (transform-style: preserve-3d) {
.scene {
perspective: 900px;
}
.card:hover {
transform: rotateY(10deg) translateZ(20px);
}
}
/* JS 特性检测 */
const has3d = CSS.supports('transform-style', 'preserve-3d');
if (has3d) { init3DCube(); } else { showStaticPoster(); }
translate3d / rotate3d / preserve-3d
perspective / perspective-origin
will-change / contain
animation-timeline,需退化
实践建议:3D 效果应当被视为“增强层”。页面在没有任何 3D 支持的环境下,依然要能正常阅读与操作。做到了这一点,你就可以放心大胆地用 3D 去提升体验。
3D 变换最佳实践清单
把全文的结论浓缩成一份可以贴在工位上的清单。
- 三层结构要清晰:外层
perspective,中层preserve-3d,内层写具体变换。 - perspective 放在父级,不要用
transform: perspective()去搭多元素场景。 - preserve-3d 必须逐层设置,中间任何一层断了,3D 就没了。
- 警惕破坏 3D 的属性:
overflow、filter、opacity、clip-path等。 - 旋转角度要克制:卡片类效果一般不超过 15°,超过就容易露出背面。
- 善用 backface-visibility:不需要看到背面时设为
hidden,既正确又省性能。 - Z 值跨度不要太大:视差层一般控制在 ±200px 内,避免眩晕。
- 环形排布用 rotateY + translateZ,顺序不能反。
- 只动画 transform 与 opacity,绝不动画
left、width、top。 - 3D 元素数量要控住:一屏内同时动画的合成层不超过 20~30 个。
- will-change 按需添加、及时释放,不要常驻、不要批量声明。
- 移动端主动降级:小屏减少 3D 元素、降低图层尺寸。
- 尊重 prefers-reduced-motion:关闭旋转与视差,保留淡入淡出。
- 用 DevTools 的 3D View 与 Layers 面板验证,而不是凭感觉。
- 在真机上测试,尤其是中低端安卓设备。
/* 1. 全局 3D 变量 */
:root {
--persp-card: 900px;
--persp-cube: 1000px;
--persp-scene: 1600px;
--tilt-max: 12deg;
--flip-dur: 0.7s;
--ease-spring: cubic-bezier(0.34, 1.3, 0.64, 1);
}
/* 2. 标准 3D 场景骨架 */
.scene-3d {
perspective: var(--persp-card);
perspective-origin: 50% 50%;
}
.space-3d {
transform-style: preserve-3d;
transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
transition: transform 0.2s linear;
}
/* 3. 翻转卡片 */
.flip-inner {
transform-style: preserve-3d;
transition: transform var(--flip-dur) var(--ease-spring);
}
.flip-face {
backface-visibility: hidden;
}
/* 4. 局部隔离,限制重绘范围 */
.card-3d, .cube, .carousel-item {
contain: layout paint;
}
/* 5. 无障碍降级 */
@media (prefers-reduced-motion: reduce) {
.space-3d, .cube, .carousel {
transform: none !important;
animation: none !important;
}
.flip-inner {
transition: opacity 0.2s linear;
}
}
3D 变换的魅力,在于它把“平面网页”变成了“可以触摸的空间”。但它同时也是一把双刃剑:用得克制,它是画龙点睛;用得泛滥,它就是性能灾难。当你能够在写下每一行 transform 之前,清楚地知道它会创建几个图层、走哪几个流水线阶段、在低端机上表现如何时,你才算真正掌握了它。
建议把上面的实验台反复玩几遍:拖一拖坐标系、拉一拉 perspective 滑块、转一转立方体、开一开 preserve-3d 的开关。空间感这件事,永远是“动手”比“读书”学得快。