第一次在别人的页面上看到那个会缓缓旋转的透明立方体时,我第一反应是:背后肯定挂了Three.js,再不济也是个WebGL库。结果打开DevTools一看,整个实现就一个HTML文件加几十行CSS,连JavaScript都没写。我当时挺受冲击的,既然别人能用纯CSS做出来,那我也应该能。但真到自己上手,我才发现这个东西看着简单,坑其实不少,尤其是几个关键属性一旦位置放错、书写顺序不对,转起来的画面立刻变成“纸片翻面”或者“果冻乱抖”——完全不是立方体该有的样子。
这篇文章就是想把我自己手搓这个透明立方体的完整过程、底层原理,以及踩过的坑原原本本讲一遍。适合那种刚刚学会了HTML和CSS基础、想用一个小项目练手的前端小白,也适合做页面装饰、可视化背景但不想引入重型库的开发者。代码我会贴全,步骤会拆开讲,每个关键点还会附上“我当时是怎么翻车的”,照着做基本能一次成功。
1. 透视、景深、旋转轴:动手前先搞懂三个关键概念
先说一个反直觉的事实:CSS里的3D,并不是真的在浏览器里渲染了一个三维模型,而是浏览器根据你给元素的位移和旋转参数,模拟出一个三维空间中的位置,再通过“透视投影”算法把结果画到二维屏幕上。这个原理和摄影差不多——场景里摆的是真三维的物体,但你看到的永远是相机拍出来的二维照片。
1.1 一句人话解释perspective:相机离物体有多远
perspective就是相机到物体的距离,单位是像素。它的作用只有一个:让离相机近的东西看起来大一点,离相机远的东西看起来小一点。
这个值直接影响观感,我用同样的立方体做过对比:
| perspective值 | 视觉效果 | 适用场景 |
|---|---|---|
| 300px | 透视极强,像用鱼眼镜头贴着物体看 | 追求夸张冲击力,但容易看出畸变 |
| 900px | 透视适中,立体感清晰、不畸变 | 新手推荐,能明显看到“远小近大” |
| 1500px | 透视变弱,接近软件里的正交视图 | 需要展示结构、不想有变形感 |
| 3000px | 几乎看不出3D,像一个平面图形 | 不太建议做旋转立方体 |
我用的是900px,这个值下看旋转能有明显的空间感,又不至于把方形面扯成梯形。你也可以先把透视调成300px看一次,再调成3000px看一次,亲身感受“距离远近”对透视强度的影响,这样以后选数值就有手感了。
1.2 transform-style: preserve-3d:3D效果的“总开关”
这个属性是小白最容易漏的。默认情况下,子元素只会老老实实贴在父元素所在的平面上,你哪怕给子元素转了90度,它也是“拍扁”之后投在父元素的二维平面上——所以整体看起来还是一个平面纸片。
只有在你直接包含3D子元素的容器上写上:
transform-style: preserve-3d;浏览器才允许子元素在真正的三维坐标里摆放。从字面理解就是“保留三维空间”,我是这么记的:它就相当于告诉浏览器,我的孩子们不是贴在墙上的海报,而是悬在半空中的立体构件。
这个属性要加在cube上,不是scene上,也不是每个face上。加错了位置,效果就出不来。
1.3 三个变换函数:translateZ、rotateX、rotateY各管什么
CSS的三维变换,最常用就三个函数:
translateZ(px):沿着Z轴移动。Z轴方向是朝向用户的——正值是靠近你,负值是远离你。放在立方体里,就是“朝哪个方向走出去一段距离”。rotateX(角度):绕着水平方向的轴旋转,效果像你把一张纸的顶部向后推、底部向前拉。rotateY(角度):绕着竖直方向的轴旋转,效果像转门或者旋转木马。
还有个关键点,很多人搞混:transform里写一串函数时,是从左往右依次执行的。也就是说rotateY(90deg) translateZ(100px)的意思是:先让元素向右转90度,面朝右侧,再沿着它此时的前方走100px。这也是后面拼立方体六面时的核心逻辑——先转方向,再走步。
X轴向右、Y轴向下、Z轴朝向屏幕外——这三个方向不用死记,写代码时打开DevTools,边改角度边看预览,几次就能形成肌肉记忆。
2. 先把六个面“摊在桌上”,再逐个折成立方体
理解了上面三个属性,剩下的就是机械操作了。立方体一共六个面,我们先把它们都写进HTML,用CSS让它们全部重叠在同一个位置,然后给每个面安排不同的“转身+走位”,最后它们就各归其位了。
2.1 HTML骨架:一个“舞台”套一个“骰子”,里面是六张“纸片”
结构上我习惯分三层:
.scene:舞台,负责提供透视,相当于相机的位置;.cube:骰子本体,负责开启3D空间并执行旋转动画;.face:每一个面,负责自己该站在哪个方位。
<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>六个face没有任何定位逻辑时,会默认按文档流从上到下排列,这可不行。我们要让它们全部叠在同一个位置,这样才能从同一个起点执行各自的变换。所以给.face加绝对定位,定死在父容器的左上角。
.cube { width: 200px; height: 200px; position: relative; transform-style: preserve-3d; } .face { position: absolute; inset: 0; width: 200px; height: 200px; }关于尺寸:如果你最终想要一个边长200px的立方体,那么每个面的width和height就是200px,而这个cube容器的宽高也设置为200px——它本质上就是六个面没展开时的“初始重叠位置”。
2.2 每张纸片的定位:先转方向,再走半边长
这是整个项目最核心的一步。立方体边长200px,那每个面只需要从中心向自己的方向走出100px,也就是边长的一半。
为什么是一半?因为每个面的初始位置都叠在立方体的正中心。要让“前面”这张纸落在立方体的正前方,它就得从中心向前走100px,加上纸片自身厚度为0,纸片中心就正好落在外表面位置上。
我的写法是统一用“先旋转,再平移”的思路,让每个面先面对自己该面对的方向,然后再往前走一步:
.face.front { transform: rotateY(0deg) translateZ(100px); } .face.back { transform: rotateY(180deg) translateZ(100px); } .face.right { transform: rotateY(90deg) translateZ(100px); } .face.left { transform: rotateY(-90deg) translateZ(100px); } .face.top { transform: rotateX(90deg) translateZ(100px); } .face.bottom { transform: rotateX(-90deg) translateZ(100px); }这里重点看一下back面的写法:先把纸片转180度,让它背对观众,然后沿Z轴走出100px,这个“走出”因为是旋转之后才执行的,所以方向是屏幕后方,纸片正好落在立方体的背面。整一套逻辑像折纸盒子:每个面先转向,再向外撑开。
2.3 六面配置速查表及验证方法
如果你想调整尺寸,只需要记住一个公式:走位距离 = 边长 / 2。下面是200px边长对应的完整配置:
| 面 | 旋转 | 平移 | 最终朝向 |
|---|---|---|---|
| front | rotateY(0deg) | translateZ(100px) | 面向观众 |
| back | rotateY(180deg) | translateZ(100px) | 面向屏幕里 |
| right | rotateY(90deg) | translateZ(100px) | 面向右侧 |
| left | rotateY(-90deg) | translateZ(100px) | 面向左侧 |
| top | rotateX(90deg) | translateZ(100px) | 面向顶部 |
| bottom | rotateX(-90deg) | translateZ(100px) | 面向底部 |
我强烈建议你先别一次把所有面都加上样式,只保留front一个面,确认它出现在正前方;再放开back,确认它在正后方;一个个验收。如果某个面位置方向不对,多半是角度正负写反了——把90度改成-90度再看一眼就好。这个阶段是打地基,后面动画和透明效果再花哨,地基歪了都是白搭。
3. 动起来的关键:动画帧、循环和“透视点”到底落在哪
六个面摆好了,这时候你刷新页面应该能看到一个静态的立方体线框,或者因为还没有半透明色,可能只看到一个最前面的面。现在让它转起来。
3.1 @keyframes怎么写才能转得优雅
最简单也最不会出错的写法,是在cube上同时旋转X轴和Y轴:
.cube { animation: spin 10s linear infinite; } @keyframes spin { from { transform: rotateX(0deg) rotateY(0deg); } to { transform: rotateX(360deg) rotateY(360deg); } }animation里四个参数分别是动画名、时长、速度曲线、重复次数。10s是一圈10秒,会觉得沉稳优雅,适合做背景装饰;如果你的立方体是页面主角,想活泼一点,改成4s或者6s就好。
旋转方向如果想反过来,把to里的角度改成-360deg就行。想让它在某个角度停一下,可以用百分比分帧,比如:
@keyframes spin { 0% { transform: rotateX(0deg) rotateY(0deg); } 50% { transform: rotateX(180deg) rotateY(180deg); } 100% { transform: rotateX(360deg) rotateY(360deg); } }但日常做匀速旋转,from到to的写法足够用了。
3.2 为什么我转起来像个“扁平纸片”:preserve-3d被拍扁了
这是遇到人数最多的一个问题:六个面静态看好像是对的,一动起来,整个立方体像一张被打印在塑料片上的贴画,转来转去都只有一个面在变换。
95%的情况是因为.cube上忘了写transform-style: preserve-3d。没有这个属性,浏览器会先把你所有的3D变换在内部算完,然后投影成一个平面再渲染,立体空间直接被吞掉了。
还有5%的情况是写了,但被覆盖了。比如某些全局CSS里给所有div设置了transform-style: flat,或者你把这个属性误写在了.face上。排查时先确认属性位置,再确认优先级冲突。
3.3 透视点应该放在“相机”身上,而不是旋转的物体身上
这个坑我踩得最深,必须重点讲。
如果把perspective写在.cube上,比如:
.cube { perspective: 900px; animation: spin 10s linear infinite; }你会看到什么?立方体确实在转,但整个画面的透视感是“漂”的,像摄影师坐在一个旋转的陀螺上跟着物体一起转,导致画面忽远忽近,甚至面的比例都跟着乱变。
正确的做法是把perspective放在外层的舞台上,也就是.scene上:
.scene { width: 300px; height: 300px; perspective: 900px; margin: 40px auto; }这样相机是固定不动的,只有立方体在转,得到的效果才符合日常经验里的“一个物体在我面前旋转”。我把这段话单独拿出来说,是因为很多人检查代码时只盯着6个face看,完全想不到是透视位置的问题。
3.4 给动画调速度:线性真的比什么都好用
新手容易把动画速度曲线调得很花。我的经验是:做匀速旋转的3D装饰,linear就是最优解。ease和ease-in-out会让立方体在每圈两端有明显的减速停顿感,看起来像在“喘气”,不是匀速旋转。
如果你确实想让动画有节奏,可以这样:
.cube { animation: spin 8s linear infinite; }先记住一个原则——展示三维物体时,相机不动、物体匀速动,才是最不容易出戏的方案。
另外,如果页面卡顿,优先检查是不是每个face都加了很重的filter或box-shadow,这些在旋转时会触发逐帧渲染,非常消耗性能。我在后面会提到透明质感的做法,注意别用过头。
4. 透明感不是“把opacity调到0.5”那么简单
很多人做透明立方体第一反应是给整个.cube加opacity: 0.5,结果发现转起来后,几个半透明面叠在一起,颜色糊成一团,棱线也看不清。这里有两个截然不同的透明层级,必须分开控制。
4.1 面的透明用rgba背景,整体透明用opacity完全是两回事
opacity是作用于整个元素包括它的所有子元素的,而且会让文字也跟着变透明,不适合用来做六面玻璃质感。
正确做法是给每个.face设置半透明的背景色和相对不透明的边框:
.face { border: 2px solid rgba(100, 200, 255, 0.8); background: rgba(100, 200, 255, 0.25); color: #fff; font-size: 20px; display: flex; align-items: center; justify-content: center; }rgba的第四个参数是alpha通道,控制这个颜色本身的透明度。background用0.25左右,会有一种“彩色玻璃”的感觉;border用0.8,保证棱线清晰可辨,不会因为透明度过高而消失。
那为什么不开opacity: 0.5呢?因为半透明背景叠加是符合物理直觉的,你透过前面一块玻璃看后面一块玻璃,本来就会看到两层颜色叠加。如果把整个立方体透明度调低,每个面都在和别的面“互相污染”,颜色层次反而更乱。
4.2 背景要选深色,否则白玻璃放在白纸上等于没做
这算是一个很“血压升高”的经验:我第一次做完,透明效果死活看不出来,以为代码写错了;后来发现是因为页面背景是白色的,半透明白色玻璃放在白色背景上,当然就像隐形了。
建议给页面或舞台背景加上深色渐变,比如:
body { min-height: 100vh; margin: 0; background: radial-gradient(circle at center, #1a1f36, #0b0e1a); display: grid; place-items: center; }立方体放上去后,棱线会显得特别清晰,蓝色半透明白玻璃的质感也能立刻体现出来。如果你不喜欢蓝色,可以换成紫红色、青色,都是科技感很强的配色。
4.3 backface-visibility到底开不开?透明立方体要保留背面
backface-visibility决定一个元素背朝你时是否可见,默认值是visible。
对于透明立方体,我建议不要改这个属性,保持默认。因为透明效果的魅力之一,就是你能从正前方看到后面那根棱线、那个面。如果设置成hidden,立方体转到一定角度时,后面几个面会突然消失,立体感和透明感都会大打折扣。
但有一种情况要开hidden:你做的不是透明立方体,而是类似“纸盒包装盒”的实心风格,每个面都要不透明、且背面的内容不能露出来,那就可以用backface-visibility: hidden。取舍标准很简单——你希不希望看到背面的东西。
4.4 用内阴影和边框提升“玻璃质感”
想让立方体看起来不那么廉价,除了背景和边框颜色,还有两个很实用的小技巧:
.face { box-shadow: inset 0 0 15px rgba(100, 200, 255, 0.3); border-radius: 4px; }内阴影会在每个面上做出一种边缘泛光的效果,像是玻璃边缘折射光线;border-radius: 4px让每条棱看起来不是锋利的折角,多了一点打磨过的质感。
但我必须提醒一句:少用filter: blur()。虽然棱线加一点高斯模糊看起来确实梦幻,但在旋转动画中,浏览器需要对每一帧做模糊计算,小屏设备会明显掉帧。如果想做发光感,优先用box-shadow,别用filter。
5. 避坑清单:我自己踩过的五个典型翻车现场
这一节我打算放几个自己亲手踩过的问题,每个都给出现象、原因和最终的解决方案。如果你做出来的东西和预期的有出入,先来这里对号入座。
5.1 翻车现场一:转起来以后像“平板翻面”
现象:能转,但完全没有立方体的体感,更像一个图片在翻身。
排查链路:先看.cube是否有transform-style: preserve-3d;再确认它没有被后续样式覆盖。你可以临时给六个面分别设置红橙黄绿蓝紫背景,如果转起来能明显看到不同颜色在换位置,就说明3D空间已经起作用了,剩下的是视觉问题。我之前一次就是这样定位的——颜色能分开,说明方向和位置都对,问题其实出在透视太强,把画面拉变形了。
5.2 翻车现场二:perspective写在cube上,画面像“陀螺仪视角”
现象:立方体转的时候,整个视觉空间也跟着晃,甚至面的大小在变化。前面的3.3节详细分析过了,解决办法就是把perspective从.cube挪到.scene。
这里再补充一个检查手段:打开DevTools的“样式”面板,把鼠标悬停在.cube的transform属性上,浏览器会显示一个3D坐标系的可视化视图。如果坐标系的原点跟着动画乱跑,那透视多半放错容器了。
5.3 翻车现场三:transform顺序写反,前后两个面“焊死”在同一侧
最初我犯过一个很经典的错误,把“先转方向再走位”写成了“先走位再转方向”。比如把右面写成:
.face.right { transform: translateZ(100px) rotateY(90deg); }这样执行过程就变成了:先沿着屏幕外的方向平移100px,再绕自身Y轴转90度。结果是这个面根本没有落到立方体右侧,而是跑到了前面再“原地打转”,整个立方体形状直接散架。
5.4 翻车现场四:立方体转快了以后,页面出现横向滚动条
旋转时立方体的角会超出舞台盒子一段距离,如果你的.scene尺寸刚好等于立方体边长,那这些突出的部分很容易把页面撑出滚动条,特别影响整体观感。
给三个建议:
.scene比.cube大一圈,比如立方体200px,舞台300px;- 给舞台加大留白
padding,别让顶角贴边; - 或者在
body上加overflow-x: hidden。
我见过有人直接把overflow: hidden加到.scene上,结果立方体转到一半边缘被硬生生切掉,效果更糟。千万别这么干。
5.5 翻车现场五:在面里写文字时,文字方向是反的
如果你打算在六个面上放字,比如做一个“CSS 3D”展示骰子,你会遇到一个细节问题:背面的字从正面看是镜像的。
因为back面先旋转了180度,而它的文字内容也跟着转了180度。解决方案是在面内部的文字容器上再做一次反向旋转:
.face.back .content { transform: rotateY(180deg); }相当于把文字在内部再翻回来。这个不细看发现不了,一旦你放有实际含义的文字,就会发现镜像字非常出戏。
6. 从“会转的方块”到“能玩的小玩具”:三个进阶方向
当你把最基础的一版做出来以后,可以沿着下面三个方向继续玩。这些都是不需要引入JavaScript就能实现的进阶玩法,非常适合用来巩固理解。
6.1 方向一:用CSS变量把尺寸、速度、颜色抽出来
CSS变量能让你的立方体变得“可配置”,也方便随时调整。我的做法是这样:
:root { --side-length: 200px; --half-length: calc(var(--side-length) / 2); --spin-speed: 10s; --glass-color: rgba(100, 200, 255, 0.25); --edge-color: rgba(100, 200, 255, 0.9); } .cube { width: var(--side-length); height: var(--side-length); animation: spin var(--spin-speed) linear infinite; } .face { width: var(--side-length); height: var(--side-length); background: var(--glass-color); border-color: var(--edge-color); } .face.front { transform: rotateY(0deg) translateZ(var(--half-length)); }以后想换成300px边长、8秒一圈,只要改--side-length和--spin-speed,所有面自动跟着变。这就是CSS变量天然的“设计系统”优势,调参效率高很多。
6.2 方向二:让六个面各自转起来
基础的立方体是整体旋转。你还可以让每个face拥有自己的动画:
.face { animation: breathe 4s ease-in-out infinite alternate; } @keyframes breathe { from { filter: brightness(0.8); } to { filter: brightness(1.4); } }或者给每个面单独设置周期性的位移,做出一种“呼吸”的节奏感。这种效果适合做个人主页的视觉装饰,看起来很灵动。
6.3 方向三:作为页面背景的“氛围角”
我现在常做的事,是把一个低透明度的立方体放在页面的右下角,并把它压到背景层:
<div class="scene" aria-hidden="true"> ... </div>.scene { position: fixed; right: 40px; bottom: 40px; width: 260px; height: 260px; pointer-events: none; }加pointer-events: none是为了不让它挡住按钮的点击区域。然后把立方体速度调慢到20秒一圈、透明度降到很低,既不影响主内容,又能在页面上制造出一种“有细节在背后流动”的高级感。
我最初做这个透明立方体,只想验证一下自己CSS 3D是不是真的懂了。做完之后反而觉得,它更像一块试金石——perspective、transform-style、transform顺序、透明叠加、动画曲线,这些平时容易忽略但非常重要的CSS机制,全都浓缩在这一个几十行的文件里。现在我的个人页面角落,那个蓝色玻璃立方体还在慢悠悠地转着,时不时还会有朋友问我是用什么渲染引擎做的,我说就一个HTML文件加几十行CSS,他们通常不太信,但这个不重要,重要的是你自己动手做完一遍之后,对CSS 3D坐标系的理解,是真的完全不一样了。