第一次写完这个旋转3D立方体的时候,说实话挺有成就感的——一个平平无奇的div,加上几行transform和animation,就能在浏览器里转出一个货真价实的立体方块。很多朋友学 CSS 学到过渡和动画这一节就卡住了,因为 2D 的东西再怎么飘来飘去、弹来弹去,始终觉得差点意思。这个 CSS 3D 立方体练习,正好是打通从"平面布局"到"空间认知"的那道门。
它解决的核心问题很简单:让你彻底搞懂perspective、preserve-3d、rotateX/rotateY这几个平时几乎用不到、但一用就懵的 CSS 属性。适合谁?刚学完 Flex 和 Grid、想挑战一点真正有视觉冲击力效果的前端初学者,或者已经在写业务代码、但从来没用过 CSS 3D 的开发者。这篇文章不讲虚的,直接把拆解思路、参数推导、完整代码和踩坑记录都摆出来,你照着敲一遍,再去调参数,比看十篇理论都管用。
1. 先搞清楚旋转3D立方体的底层逻辑
1.1 所谓3D,其实就是"近大远小"的欺骗术
浏览器本身是一个二维平面,CSS 再怎么折腾,最终也是把内容画在一个矩形屏幕上。那为什么我们能看出"立体感"?靠的就是透视(perspective)。这个概念你其实从小就懂:铁路轨道在远处会交汇成一个点,路边的电线杆越远越小,这就是"近大远小"。CSS 3D 做的正是这件事——通过数学计算,让离你近的面显示得大一点,离你远的面显示得小一点,大脑一收到这个信号,就自动脑补出一个三维空间。
所以你在代码里看到的perspective: 800px,它的意思是"观察者站在距离这个元素 800 像素远的地方看它"。这个距离非常关键,我后面会单独说。你先记住一句话:没有 perspective,就没有 3D,只有平面图形被拉伸变形的错觉。
1.2 立方体拆解:六个面与坐标轴
一个立方体有六个面:前、后、左、右、上、下。在 CSS 里,这六张"面皮"本质上就是六个普通的div,它们一开始全部重叠在一起,然后我们通过旋转和位移,把它们搬到各自该待的位置上。
理解坐标轴是这一步的关键。CSS 3D 里有三条轴:
- X 轴:水平方向,向右为正。
rotateX是绕 X 轴转,你可以想象成一只手捏住立方体的左右两侧,让它上下翻转。 - Y 轴:垂直方向,向下为正。
rotateY是绕 Y 轴转,想象成捏住上下两侧,让它左右旋转。 - Z 轴:指向屏幕外,朝向你为正。
translateZ(正数)是把元素往你眼前拉,translateZ(负数)是往屏幕里面推。
这个坐标系我建议你就在桌面上比划一下:拿一个快递纸箱,先定义好哪面是前、哪面是上,然后试着用左手的拇指、食指、中指比划三条互相垂直的轴。只要这个空间感建立起来了,后面所有代码你都能在脑子里"看到"画面,而不是瞎猜参数。
1.3 为什么推荐用纯 CSS 来做
可能有人会说:3D 效果用 Three.js 不香吗?WebGL 不香吗?香,但对于一个练习项目来说,纯 CSS 恰恰是理解 3D 原理最好的路径。
Three.js 帮你把矩阵运算、相机、光照全部封装好了,你只要写"创建一个立方体、添加材质、旋转",三行代码就出效果,但你对"为什么 rotateX(90deg) 之后要再 translateZ(100px)"这件事毫无感知。CSS 3D 则把所有变换暴露在你面前,每一个角度、每一段位移都要你自己算、自己调,它逼着你理解几何关系。
而且纯 CSS 能做到的效果其实相当可观:旋转立方体、卡片翻转、3D 轮播图、产品展示台、翻书效果,这些日常业务里完全够用了,不需要为了一个卡片翻转就去引一个几十 KB 的 3D 库。这个练习做完,你获得的不只是一个会转的方块,而是以后看到任何 CSS 3D 效果都能快速拆解的能力。
2. 三个核心参数,决定3D效果成败
2.1 perspective:站在多远看这个盒子
perspective是 3D 效果的第一关,我见过太多人写了半天发现"怎么没有立体感",十有八九是忘了加它,或者加错了位置。
它的取值决定了透视的强烈程度。拿perspective: 800px来说,观察者离元素越近(数值越小),近大远小的效果越夸张,物体形变也越严重;离得越远(数值越大),透视越平缓,物体越接近"轴测图"那种呆板感。
我实测下来的手感是:
400px:透视感极强,近处的面大得压迫,适合表现冲击力,但形变严重,文字会拉伸得很厉害800px:比较舒服的默认值,透视感明显但不夸张,适合大多数场景1200px以上:透视感很弱,适合需要"建筑图纸"般精确感的展示场景
这里有一个新手最容易踩的坑:perspective到底是加在父元素上还是子元素上?答案是加在父元素(3D 舞台)上。如果你给子元素直接写transform: perspective(800px) rotateY(...),那是对单个元素单独取透视,六个面各自为政,你看到的就不是一个完整的立方体空间。正确写法是给包裹六个面的容器设置perspective,让所有面共享同一个观察视角。
提示:
perspective只影响直接子元素的 3D 变换,想嵌套多层 3D 结构时,每一层都要考虑是否接受透视。
2.2 transform-style: preserve-3d:让子元素待在三维世界里
这是第二个容易漏的属性。默认情况下,CSS 会把元素的 2D 变换扁平化处理——即使你给子元素写了rotateX(45deg),它渲染出来也只是在父元素平面上做了一个斜向拉伸,并没有真正"立"起来。当父元素设置transform-style: preserve-3d之后,子元素才会被保留在三维坐标系中,形成一个真正的 3D 空间。
你可以把preserve-3d理解成一句话:"给孩子们一个真正的三维舞台,别把它们按回平面上。"
需要特别注意,preserve-3d和两个属性是冲突的:overflow: hidden和filter会把preserve-3d强行降级成flat。如果你发现自己的立方体突然变得扁平,先检查是不是加了overflow: hidden或者filter: blur(...)。我一度为了给立方体外层加一个圆角遮罩,顺手写了overflow: hidden,结果整个立方体瞬间变成一张纸片,排查了半天才找到元凶。
2.3 transform-origin 与 backface-visibility 的辅助作用
transform-origin设置变换的基准点,默认是元素的中心点50% 50% 0,对立方体来说默认就够用。但如果你做的是"门打开"这种效果,基准点在元素的左侧边缘,就需要设置transform-origin: left center。理解它对扩展很重要,至少知道有这个东西存在。
backface-visibility则控制"元素背面是否可见"。前、后、左、右、上、下六个面,当立方体旋转时,背对着你的那一面如果不设置任何属性,你会看到它的镜像内容(文字是反的),很穿帮。设置backface-visibility: hidden,当某个面的背面朝向你时,浏览器直接不渲染它,效果干净很多。
不过有一个实用场景反而故意不隐藏背面:如果你做一个玻璃质感的透明立方体,希望看到内部的结构线,那就不要加hidden,六个面全部半透明,背面若隐若现反而更酷。
3. 从零到转起来,完整实操步骤
3.1 先搭HTML骨架
动手写代码。HTML 结构非常直观:一个舞台容器,一个立方体容器,六个面。
<div class="scene"> <div class="cube"> <div class="face front">前</div> <div class="face back">后</div> <div class="face left">左</div> <div class="face right">右</div> <div class="face top">上</div> <div class="face bottom">下</div> </div> </div>scene是透视舞台,cube是 3D 空间载体,face是六个面。命名上我习惯用语义化的类名,调试的时候一眼能认出是哪个面在出问题。
3.2 核心CSS:让六个面各就各位
整个练习的技术核心就在这一节,我建议你边敲边在脑子里想象每个面是怎么"飞"到位置上去的。
首先定义舞台和立方体的基础样式:
.scene { width: 200px; height: 200px; margin: 100px auto; perspective: 800px; } .cube { width: 200px; height: 200px; position: relative; transform-style: preserve-3d; /* 先把整个立方体平移到页面中间,方便观察 */ transform: translateZ(-100px); } .face { position: absolute; width: 200px; height: 200px; border: 2px solid #333; font-size: 40px; font-weight: bold; display: flex; align-items: center; justify-content: center; }这里先解释一个关键操作:.cube的transform: translateZ(-100px)是干嘛的?因为六个面要围绕中心点旋转,而默认的旋转中心是(100px, 100px, 0)这个点(也就是 cube 元素的中心),如果六个面都沿着自己的中心旋转后再平移,会发现立方体的中心在参照物上稍微偏前。先把整个立方体向后推 100px,是为了让立方体的几何中心正好落在页面可视区域的中心,观察起来更舒服。
然后给每个面写变换。这是最需要理解的一段,我先把代码放出来:
.face.front { transform: rotateY(0deg) translateZ(100px); } .face.back { transform: rotateY(180deg) translateZ(100px); } .face.left { transform: rotateY(-90deg) translateZ(100px); } .face.right { transform: rotateY(90deg) translateZ(100px); } .face.top { transform: rotateX(90deg) translateZ(100px); } .face.bottom { transform: rotateX(-90deg) translateZ(100px); }你可能会有疑问:为什么不是translateZ(100px)然后再rotateY?这涉及 CSS 变换的顺序问题——变换是从右往左应用的。先执行rotateY(90deg),面先原地旋转 90 度,然后再执行translateZ(100px),把已经旋转了 90 度的面沿着 Z 轴正向推 100px。因为旋转发生在平移之前,所以平移的方向也带着旋转后的方向,于是"前"面被推到了右边,"后"面被推到了左边,正好是六个面向外展开的效果。
这个顺序极其重要,如果你把顺序反过来写成translateZ(100px) rotateY(90deg),先平移再旋转,面会先跑到立方体中心前方 100px,然后以自身中心为轴旋转,最后落在一个错误的位置上,六个面全部挤在一起。我当初调试的时候在这里卡了半个多小时,最后是拿纸笔画出每一步的坐标才搞明白。
画个简单的坐标思考图(想象你正对着屏幕):
front:不旋转,直接向 Z 轴正方向推 100px,这就是离你最近的那一面back:先绕 Y 轴转 180 度,让面朝向背面,再向 Z 轴推进 100px,它落到了最远处,而且面朝屏幕内侧left:先绕 Y 轴转 -90 度,面朝左,再推 100px,落在了左侧right:先绕 Y 轴转 90 度,面朝右,再推 100px,落在了右侧top:先绕 X 轴转 90 度,面朝天,再推 100px,落在了上方bottom:先绕 X 轴转 -90 度,面朝地,再推 100px,落在了下方
为什么位移距离是100px?因为立方体的边长是 200px,从中心到任何一个面的距离就是边长的一半,也就是 100px。这个值要记住:你定义的盒子尺寸是多少,位移距离就是尺寸的一半。如果你把边长改成 300px,所有面的translateZ就要同步改成 150px,否则六个面会互相穿透或者留出缝隙。
3.3 用@keyframes让立方体转起来
六个面归位之后,你看到的是一个静态的、有立体感的立方体。下一步就是让它动起来。这里用 CSS 动画实现双轴旋转:
@keyframes cube-spin { from { transform: translateZ(-100px) rotateX(0deg) rotateY(0deg); } to { transform: translateZ(-100px) rotateX(360deg) rotateY(360deg); } } .cube { animation: cube-spin 8s linear infinite; }注意我把translateZ(-100px)也写进了动画的每一帧,因为动画的transform会覆盖元素原本直接写的transform,如果你在.cube上单独写了transform: translateZ(-100px)然后在动画里只写旋转,动画一运行,位移就丢了,立方体会跳动一下。这是动画覆盖的经典坑。
linear是匀速运动,适合机械感的效果。如果你想要"转一圈停一下再转"的节奏感,可以换成cubic-bezier(0.4, 0, 0.2, 1),或者把关键帧写复杂一点:
@keyframes cube-spin { 0% { transform: translateZ(-100px) rotateX(0deg) rotateY(0deg); } 25% { transform: translateZ(-100px) rotateX(90deg) rotateY(90deg); } 50% { transform: translateZ(-100px) rotateX(180deg) rotateY(180deg); } 75% { transform: translateZ(-100px) rotateX(270deg) rotateY(270deg); } 100% { transform: translateZ(-100px) rotateX(360deg) rotateY(360deg); } }这样每一段 25% 的关键帧之间会有一次短暂的停顿感,看起来像"咔哒、咔哒"一格一格地翻面,比纯匀速更有趣味。
3.4 给立方体加点配色和文字,验证每个面
为了让旋转效果更容易观察,建议给每个面加不同的背景色,并保留文字标记。这不仅是美观问题,更是调试手段——当你看到旋转过程中的颜色顺序,就能快速判断哪个面的朝向是否正确。
.face.front { background: rgba(255, 99, 132, 0.8); } .face.back { background: rgba(54, 162, 235, 0.8); } .face.left { background: rgba(255, 206, 86, 0.8); } .face.right { background: rgba(75, 192, 192, 0.8); } .face.top { background: rgba(153, 102, 255, 0.8); } .face.bottom { background: rgba(255, 159, 64, 0.8); }加上半透明背景色的效果:颜色之间会叠加,转起来的时候你能隐约看到内部结构,视觉效果更丰富。如果想让每个面在转到背面时不显示,在.face上加一行backface-visibility: hidden;即可。
到这里,一个最基本的旋转 3D 立方体已经完成。把代码在浏览器里打开,你看到的是一个不断旋转、六个彩色面清晰可辨的立方体。
4. 进阶玩法:交互、光影和性能优化
4.1 鼠标拖拽控制旋转方向
动画一直旋转很炫,但用户无法控制观察角度,玩一会儿就腻了。加一个鼠标拖拽交互,体验立刻不一样。这里需要一点点 JavaScript,但逻辑非常轻量。
思路是:监听鼠标按下和移动事件,把鼠标横向移动的距离映射为rotateY,纵向移动的距离映射为rotateX。核心代码如下:
const cube = document.querySelector('.cube'); let isDragging = false; let rotationX = 0; let rotationY = 0; let startX = 0; let startY = 0; document.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; rotationX += -deltaY * 0.5; rotationY += deltaX * 0.5; cube.style.transform = `translateZ(-100px) rotateX(${rotationX}deg) rotateY(${rotationY}deg)`; startX = e.clientX; startY = e.clientY; }); document.addEventListener('mouseup', () => { isDragging = false; });细节说明:rotationX用-deltaY,是因为鼠标往下拖时我们希望立方体向上翻转(自然交互逻辑)。0.5是旋转灵敏度,数值越大,鼠标移动同样距离,立方体转的角度越大。
这里又会出现动画覆盖的问题:如果你在 CSS 里写了animation: cube-spin 8s linear infinite;,那 JavaScript 写的style.transform无法生效,因为动画的优先级高于内联样式。解决方法是:拖拽时给立方体设置animation: none,或者干脆把自动旋转关掉,完全交给鼠标控制。这是交互和动画经常打架的地方,提前了解能省不少排查时间。
4.2 空间感升级:添加光影和半透明面
一个纯色方块转起来虽然立体,但缺少"质感"。要让视觉效果上一个台阶,可以做两件事:
第一,给每个面加渐变背景,模拟统一光源。比如所有面的灯光都从左上角来:
.face { background: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(100,149,237,0.7) 100%); }第二,让立方体"自带阴影"。在.cube上加transform-style不影响自身阴影,但你可以给每个面单独设置渐变,或者用一个伪元素模拟投影。更简单有效的做法是用 CSS 的drop-shadow滤镜直接作用于整体:
.cube { filter: drop-shadow(20px 20px 30px rgba(0, 0, 0, 0.3)); }不过前面提醒过,filter会导致preserve-3d失效。所以这里不能直接对.cube用filter,要对最外层的.scene用,或者在.face上直接加box-shadow。实测下来,每个面加box-shadow: inset 0 0 30px rgba(0,0,0,0.2)能产生很好的内部阴影过渡,显得每个面是一个有厚度的实体。
4.3 性能避坑指南
CSS 3D 动画如果卡顿,通常不是电脑性能不够,而是触发了大量重绘和重排。这里有几个硬经验:
- 只动画 transform 和 opacity。不要去动画
left、top、width这些属性,它们会触发布局计算,每帧都算一次,必然掉帧。 - 给
.face添加will-change: transform,提前告诉浏览器这个元素的变换会被频繁修改,让它做优化。但不要滥用,一个页面同时只有少数元素设置will-change,否则内存占用飙升。 - 6 个面的 3D 变换其实很便宜,真正的性能瓶颈在阴影和滤镜。如果发现动画卡,优先检查是不是每个面都加了多层
box-shadow。 - 移动端对 3D 的支持并不差,关键是避免大尺寸元素做 3D 变换。200px 的立方体随便转,2000px 的立方体就开始烫手了。
5. 真实踩坑记录与排查速查表
5.1 五个高频问题
我把自己做这个练习时踩过的坑和帮别人调试时遇到的问题整理成了一张速查表,直接对照着排查:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 六个面全部叠在一起,看不出立方体 | 父元素没加perspective或transform-style: preserve-3d | 在.scene上加perspective: 800px,在.cube上加preserve-3d |
| 立方体存在,但某个面方向不对 | 该面的rotate方向和translateZ顺序写反了 | 变换顺序必须是rotate在前、translateZ在后 |
| 动画开始后立方体位置突然跳动 | 动画的 transform 覆盖了元素原本的 transform | 把位移写进动画关键帧的每一帧里 |
加了overflow: hidden后立方体变平 | overflow: hidden会把preserve-3d强制降级为flat | 把overflow移到preserve-3d的外层容器上 |
| 文字在背面显示为镜像 | 没有设置backface-visibility | 给.face添加backface-visibility: hidden; |
5.2 实用的调试习惯
最后分享几个我实际用下来效率很高的调试技巧,可能比直接看文档更有用:
第一,改完参数立刻看效果,然后疯狂调数值。perspective从 400px 调到 1500px,rotateX从 20度 调到 70度,感受每一个数值变化对画面的影响。这个"手感"非常重要,真正理解了数值和视觉效果之间的关系,以后做任何 3D 效果都能快速猜出大概参数。
第二,用浏览器开发者工具实时修改 transform。在 Elements 面板选中.cube,直接在右侧 Styles 里改transform的值,每个面挨个测试,不用刷新页面就能看到结果。调试动画时也可以打开 Performance 面板录一段,看看有没有掉帧。
第三,给每个面加上不同的文字和背景色再调试。如果你看到"左"这个字跑到了前面,说明rotateY(-90deg)的方向理解反了。这种"带标签调试"比看空盒子快得多。
我个人的体会是:做这个小项目最重要的不是背出六个面的变换代码,而是理解"旋转 + 平移"这两个操作组合在三维空间中的几何意义。当你脑子里能浮现出一个面先旋转、再沿着某个轴移动的画面时,CSS 3D 的大门就算是真正打开了。最后再分享一个小技巧——做完了静态版和动画版之后,尝试把立方体的边长改成 300px,把所有参数重新算一遍,你会发现原来那些"魔法数字"其实全都有据可循。这比再复制十个别人的代码都有用。