把“透明立方体”这个标题放在前端新手练习清单里,几乎是必做项。HTML加CSS,不碰任何JavaScript,就能让一个六个面的透明盒子在页面上旋转起来,这件事听起来很酷,做起来也确实不难。但难的是“看着简单,一写就翻车”——我见过太多人卡在透视失效、面乱飞、透明度糊成一团这些问题上,所以这篇就把完整的实现思路、步骤和踩过的坑一次说清楚。不管你是刚学完HTML和CSS基础,还是正在准备前端面试,花半小时把这个小项目做一遍,CSS的3D变换你会摸得很透。
1. 项目拆解:这个立方体到底在转什么
先说结论:CSS里的3D立方体,并不是浏览器真的渲染了一个立体物体,而是我们用六个平面矩形,通过位移和旋转,拼出了一个“看起来是立方体”的组合。这个认知很关键——你会发现CSS的3D不像Three.js那样有“体积”概念,所有东西都是一个一个的平面元素,只是通过transform属性让它们在三维坐标系里摆放、转动、重叠。
1.1 从2D到3D:三个关键属性
平面变立体的核心,是perspective、transform-style: preserve-3d和transform这三个属性组合出来的空间感。
perspective给元素加上透视距离,单位是px。它模拟的是“人眼离屏幕有多远”,值越大,物体看起来越平,值越小,透视形变越夸张。就好比你离一个盒子10米看,它几乎没有透视变形;凑到10公分看,侧面的缩小感就非常明显。给父容器设置perspective: 1200px左右,是比较舒服的观感。transform-style: preserve-3d,这是最容易漏掉的一步。默认情况下,CSS会把子元素压扁成2D平面去绘制,只有设置了preserve-3d,子元素才能保持自己在三维空间中的位置。它的作用就像是告诉浏览器:不要把这些面渲染成一层纸,要按照立体坐标去计算遮挡和透视。transform属性里可以组合多个函数,比如rotateX(45deg) translateZ(100px)。这里有个初学者必踩的坑:函数的顺序不是随便写的,因为后写的变换是基于先写的变换结果去执行的。比如先rotateY(90deg)再translateZ(100px),那么这个元素是沿着旋转后的“新Z轴”移动,效果上相当于移动到了旋转后的位置;反过来先移动再旋转,移动方向还是原始的坐标轴,结果完全不同。做立方体时,我们通常会先旋转面,再沿Z轴推出去,这样每个面才能被摆到正确位置。
1.2 立方体六个面的坐标设计
假设我们要做一个边长200px的立方体,那么每个面都是一个200px×200px的div。它们初始都叠在一起,位置在立方体的中心。为了让它们变成六个面,需要这样处理:
- 前面:不旋转,直接
translateZ(100px),即向观察者方向拉近。 - 后面:先
rotateY(180deg),再translateZ(100px)。注意旋转180度后面朝向反了,但Z轴方向也反了,推出去的位置刚好是立方体背后。 - 左面:先
rotateY(-90deg),再translateZ(100px)。 - 右面:先
rotateY(90deg),再translateZ(100px)。 - 上面:先
rotateX(90deg),再translateZ(100px)。 - 下面:先
rotateX(-90deg),再translateZ(100px)。
这个“先旋转、再沿Z轴平移”的方法,本质上是把每个面当作“从中心往六个方向各推半边长”。因为旋转会改变局部坐标轴的方向,后面、左面、右面这些面旋转后,它们的局部Z轴已经指向立方体外侧,所以统一用translateZ(100px)就能精准推到正确位置。这个100px就是边长的一半,在实际代码里建议用CSS变量,这样改边长时不用每个面单独调。
2. 手把手搭建页面基础结构
这一步我们先不纠结动画,而是把一个静态的、能看到三维形状的立方体先搭出来。很多教程一上来就写动画,结果新手根本分不清是面位置错了还是动画写错了。我的习惯是:先静态摆好,再让它动。
2.1 HTML骨架与类名规划
HTML结构特别简单,一个.scene容器负责透视,一个.cube容器负责3D空间,内部放六个面。这里强调一下为什么需要两层结构:.scene上设置perspective,.cube上设置transform-style: preserve-3d。如果直接在.cube上同时加透视和3D空间,动画会容易出问题。
<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>每个面里可以放一个文字,用来验证面的朝向是否正确。当立方体转到某一面时,文字应该能正对着你,如果出现镜像或颠倒,说明旋转顺序或backface-visibility处理得不对。
2.2 CSS变量与基础样式
使用CSS变量来管理尺寸,这样后期调整非常舒服。用--size表示边长,--half-size计算边长的一半。注意CSS变量里做运算需要calc(),而且calc里的乘除支持有限,但除以2可以直接写成calc(var(--size) / 2),浏览器兼容性没问题。
:root { --size: 200px; --half-size: calc(var(--size) / 2); } .scene { width: 300px; height: 300px; margin: 100px auto; perspective: 1000px; /* 透视距离 */ } .cube { width: var(--size); height: var(--size); position: relative; transform-style: preserve-3d; /* 开启3D空间,关键 */ } .face { position: absolute; width: var(--size); height: var(--size); border: 1px solid rgba(0, 0, 0, 0.4); background: rgba(255, 255, 255, 0.3); /* 半透明 */ display: flex; align-items: center; justify-content: center; font-size: 20px; color: #333; }这里.face用position: absolute是为了让六个面都重叠在.cube的中心点,否则它们会依次往下排。设置position: relative给.cube,是为了让绝对定位的子元素以它为基准。这些细节初学者很容易漏,但漏掉一个整个效果就崩了。
然后写每个面的位置。同样以200边长为例,统一用translateZ(100px),旋转角度按前面坐标设计来。
.front { transform: rotateY(0deg) translateZ(var(--half-size)); } .back { transform: rotateY(180deg) translateZ(var(--half-size)); } .right { transform: rotateY(90deg) translateZ(var(--half-size)); } .left { transform: rotateY(-90deg) translateZ(var(--half-size)); } .top { transform: rotateX(90deg) translateZ(var(--half-size)); } .bottom { transform: rotateX(-90deg) translateZ(var(--half-size)); }写完这些后刷新页面,你看到的可能不是一个好看的立方体,而是一个奇奇怪怪的六边形、或者只有某个面特别大。别慌,很可能是两个原因:要么是.cube没有在.scene里居中,要么是透视距离太近导致画面被裁切。给.scene加display: flex; align-items: center; justify-content: center;就能解决大部分布局问题。
3. 动画实现:让立方体自己转起来
静态立方体摆对位置后,加动画就很简单了,核心是@keyframes配合animation。但对于新手,这里有几个细节值得说透。
3.1 关键帧动画与组合旋转
我们希望立方体绕Y轴转动,这样看起来像放在转盘上;如果绕X和Y同时转,就是那种“翻滚”效果。最常见的写法是:
@keyframes spin { from { transform: rotateX(0deg) rotateY(0deg); } to { transform: rotateX(360deg) rotateY(360deg); } } .cube { animation: spin 8s infinite linear; }注意这里给.cube添加动画,是让整个立方体旋转,不是给每个面分别加动画。如果你在.face上加旋转动画,六个面会各转各的,立方体形状立刻散架。另外,我的经验是动画加到.cube上,而不是.scene上,因为.scene如果转了,会连同透视关系一起转,物体看起来像是在屏幕外翻滚,而不是原地转动。
动画用infinite让它一直转,linear保持匀速,这样旋转不会忽快忽慢。如果你想要那种“飘浮旋转”的柔和感,可以试试ease-in-out,但一般玩具感强的场景用linear更稳。
这里还有一个容易忽视的点:transform的初始值是none,但动画里写了from,浏览器会自动从定义的状态开始。如果你在.cube上又写了静态的transform,比如rotateX(-15deg),这个静态值是不会跟动画叠加的,动画的from/to会完全覆盖静止的transform。所以想让立方体一开始有个倾斜的初始视角,正确做法是在from里写上这个倾斜角度,比如:
@keyframes spin { from { transform: rotateX(-15deg) rotateY(0deg); } to { transform: rotateX(-15deg) rotateY(360deg); } }这样立方体会保持一个微微俯视的角度,同时绕Y轴旋转,看起来更像一个真实物体。
3.2 鼠标交互:拖拽旋转加一点料
如果不想让立方体一直自动转,而是希望用户按住鼠标拖动来控制旋转方向,那么靠纯CSS动画就不行了,得加一点JavaScript。这里我不展开长篇大论,只给你一个最短可用的思路,因为很多新手做到这步后,会想“能不能交互一下”。
核心逻辑是监听鼠标按下和移动事件,用鼠标移动的增量去累加rotationX和rotationY,然后实时设置.cube的transform。为了避免性能问题,推荐用requestAnimationFrame。
const cube = document.querySelector('.cube'); let isDragging = false; let lastX = 0; let lastY = 0; let rotX = -20; let rotY = 0; document.addEventListener('mousedown', e => { isDragging = true; lastX = e.clientX; lastY = e.clientY; }); document.addEventListener('mousemove', e => { if (!isDragging) return; const deltaX = e.clientX - lastX; const deltaY = e.clientY - lastY; rotY += deltaX * 0.3; rotX -= deltaY * 0.3; lastX = e.clientX; lastY = e.clientY; cube.style.transform = `rotateX(${rotX}deg) rotateY(${rotY}deg)`; }); document.addEventListener('mouseup', () => { isDragging = false; });注意当鼠标拖动时,要把cube上的动画停掉。否则CSS动画的transform优先级高于JS设置的内联样式,你拖不动。最简单的做法是在mousedown事件里执行cube.style.animation = 'none',然后在松开鼠标后按需重新开启。这里有个取舍:如果你只是练习CSS,完全可以忽略这节,等后面做3D场景时再考虑交互。
4. 透明效果与层级渲染的避坑指南
透明立方体的“透明”并不是指整个立方体完全隐掉,而是让每个面像有色玻璃一样,能看到对面的面和背景。说起来容易,做起来会出现几个非常影响感官的问题,这里列出来逐个说。
4.1 透明度叠加带来的“假实心”问题
如果你给每个面的背景色设置为rgba(255, 255, 255, 0.5),转起来后会感觉立方体糊成一团白,根本分不清哪个面在哪。原因很简单:虽然每个面是50%透明,但当后面的两个面叠在一起时,透过前面那个面看到的颜色其实是“前面面的半透明色+后面面的半透明色”,视觉上透明度变成了大约0.5 + 0.5 * 0.5 = 0.75的不透明白,三个四个面叠起来就更白了。
解决方案有两个方向:
一是降低每面的透明度,比如改成rgba(255, 255, 255, 0.15)。因为立方体最多会同时有三个面被看到,15%透明度叠加后大概40%,依然能透出背景,同时又保留了“玻璃”质感。我测试下来,0.12~0.2是最舒服的范围。
二是不要用统一透明度,给每个面稍微不同的透明度,比如前面透明度高一点,后面低一点。但这种方式做视觉艺术可以,做练习没必要。一个更通用的技巧是使用rgba(255, 255, 255, 0.1)作为底色,再搭配半透明边框和阴影来勾勒棱边,这样既保持通透,又不会糊。
4.2 边框与光泽:让“透明”看得见
纯透明的面没有视觉边界,看起来就是一块颜色极淡的方块在旋转,完全没有立方体的空间感。所以透明立方体通常要借助边框、背景渐变或者box-shadow来“描边”。
最基础的做法是给.face加上border: 1px solid rgba(30, 30, 30, 0.4)。边框会跟着面一起旋转,从而形成立方体的“棱线”,立体感立刻提升一个档次。
如果想让透明效果更精致,可以给每个面加一个对角渐变或者高光。比如:
background: linear-gradient( 135deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0.05) 50%, rgba(255, 255, 255, 0.2) 100% );这种渐变会让每个面的光感不同,旋转时能明显看出是哪个面在朝向你。如果你做了毛玻璃效果(backdrop-filter),那更好看,但也要注意性能开销,后面会提到。
另外还有一个关于文字的体验问题:当右边这个面旋转到背后时,如果这个面上有文字,你会看到文字左右颠倒,就好像从纸张背面看字一样。解决办法很简单,给.face加backface-visibility: hidden,背面朝向观察者时直接隐藏该面,自然就看不到镜像文字了。但要注意:如果所有面都设置了backface-visibility: hidden,那么你透过透明的前面看后面时,后面那个面会被隐藏掉,导致立方体内部空掉。这其实是合理的,因为半透明玻璃立方体通常不需要看到背面内壁。如果你希望更真实的玻璃效果,就不加hidden,而是想办法让文字双面显示,比如用两层元素。作为一个练习,backface-visibility: hidden是首选,省心又好看。
5. 常见问题与排查技巧实录
下面这些坑是我自己写CSS 3D时踩过、以及帮别人调试时经常遇到的,整理成速查表,建议对照着排错。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 立方体看不到,只有一个面或一条线 | 透视距离太近,或者.cube没预设尺寸 | 增大perspective到800px以上,确认.cube有宽高 |
| 六个面完全重叠,像一张纸 | transform-style没有设置成preserve-3d | 给.cube加上transform-style: preserve-3d |
| 个别面位置飘了 | transform函数顺序错误 | 统一把rotate写在前面,translateZ写在后面 |
| 旋转时整个画面都跟着移 | 动画写在了.scene上 | 动画应该写在.cube上 |
| 鼠标拖不动立方体 | CSS动画覆盖了JS内联样式 | 拖拽前把animation设为none |
| 半透明面叠得发白 | 透明度太高 | 把背景透明度降到0.15以下 |
| 立方体转动时明显掉帧 | 3D绘制压力大或触发了重排 | 给.cube加backface-visibility: hidden,并尝试transform: translateZ(0)提升独立图层 |
| 动画不执行 | animation简写里缺少时间或名称 | 至少要有animation-name和animation-duration |
5.1 调试3D变换的“作弊技巧”
每次做完一个面,我都不急着让动画转起来,而是先在浏览器开发者工具里,把.cube的animation暂停,然后把@keyframes里的from态先套上去。更粗暴的办法是:临时给所有面加一个很显眼的纯色背景,比如红色、蓝色、绿色各不同。这样哪个面跑到哪,一目了然。调试完成后,再换回半透明的样式。
另一个实用技巧是逐个检查面的最终变换状态:在DevTools里点击某个.face元素,在Styles面板里看到它最终的transform值。你可以临时把translateZ改成0,看它的旋转后位置,从而判断旋转中心对不对。这个小技巧能省很多时间。
如果立方体转起来时某些面出现“穿模”或突然消失,常见原因是backface-visibility设置不一致,或者透视容器本身被添加了overflow: hidden。.scene上千万不要写overflow: hidden,它会直接剪辑掉旋转到外围的面。
5.2 性能与兼容性小贴士
CSS 3D在大多数现代浏览器里表现都不错,但有几个坑需要注意。
移动端部分浏览器对preserve-3d的兼容性有限,尤其是老版本浏览器,可能出现子元素被强制扁平化的情况。常见做法是给.cube增加-webkit-transform-style: preserve-3d前缀,但要注意某些浏览器对前缀支持也有限。真正保险的做法是:不要用太深的3D嵌套层级,有报告显示preserve-3d在部分浏览器里不能与overflow、filter等属性共存。所以不要给.cube加filter,也不要在它的祖先容器上写overflow: hidden或backdrop-filter。
性能方面,旋转的立方体如果每个面都有文字、渐变、阴影,在低端设备上可能会有掉帧。触发GPU加速的最简单方法是在动画开始前给.cube设置transform: translateZ(0),但这会改变初始位置,更好的做法是使用will-change: transform来提示浏览器提前优化。
.cube { will-change: transform; }注意will-change不要滥用,这里一个元素没问题。透明立方体本身并不复杂,只要不叠加过多滤镜,性能是能保证的。
最后再分享一个个人体会
我第一次写透明立方体时,卡得最久的就是透视到底该放在哪个元素上。后来总结成一句话:透视是“怎么看”,3D空间是“放哪里”。所以perspective永远放在父容器.scene上,preserve-3d永远放在包含所有面的.cube上。记住这句话,这个项目就成功了一半。
还有个小技巧是,把边长、透明度、旋转速度都抽成CSS变量。后面你想把200px改成400px,或者让转速快一点,只需要改一个变量,不用满文件找。把这种变量化习惯保留到以后写组件,收益会越来越大。
如果你想在这个基础上继续练手,可以试试给六个面分别贴上不同的背景图,或者做成一个相册播放器;也可以把鼠标拖拽的JS升级成支持触摸事件,让手机也能玩。CSS 3D的边界远不止一个立方体,做通这一个项目,你就掌握了浏览器里三维表现的基本功。