第一次在别人的页面上看到那个会缓缓旋转的透明立方体时我第一反应是背后肯定挂了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-3d3D效果的“总开关”这个属性是小白最容易漏的。默认情况下子元素只会老老实实贴在父元素所在的平面上你哪怕给子元素转了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 classscene div classcube div classface front前/div div classface back后/div div classface left左/div div classface right右/div div classface top上/div div classface 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边长对应的完整配置面旋转平移最终朝向frontrotateY(0deg)translateZ(100px)面向观众backrotateY(180deg)translateZ(100px)面向屏幕里rightrotateY(90deg)translateZ(100px)面向右侧leftrotateY(-90deg)translateZ(100px)面向左侧toprotateX(90deg)translateZ(100px)面向顶部bottomrotateX(-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 classscene aria-hiddentrue ... /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坐标系的理解是真的完全不一样了。