一个看似简单的 3D 立方体,背后藏着视口单位、行内块级元素的天坑、Flex 弹性布局的奥义,以及 CSS 3D 变换的组合拳。本文用三个 Demo 带你一次性搞懂它们。
开篇:不止是 3D,更是一场布局基础课
提起网页 3D,很多人会想到 Canvas。没错,HTML5 新增的 <canvas> 标签配合 JS API 可以在浏览器中绘制出极其复杂的 3D 场景,是 WebGL 的根基。但今天我们要聊的,是另一种更「轻」的 3D 实现方式 —— CSS 3D。
它通过 CSS 属性直接触发 GPU 加速,不仅限于 3D 效果,很多时候我们甚至会手动给 2D 界面加上 transform: translateZ(0) 来强制开启 GPU 渲染,优化动画性能。
本文将从一个 纯 CSS 3D 旋转立方体 项目出发(见 1.html + common.css),把沿途涉及的布局、单位、盒模型等知识点逐一拆解,并配合 2.html 和 3.html 两个独立示例,让你彻底理解:
-
vh/vw 视口单位如何让布局自适应移动端
-
行内、块级、行内块级元素的爱恨情仇
-
Flex 弹性布局的格式化上下文与 flex:1 的秘密
-
如何一步步搭建 3D 空间并组合面片构成立方体
读完本文,你收获的不仅是一个炫酷的立方体,更是一套扎实的 CSS 布局内功。
一、舞台搭建:视口单位与完美居中
先看我们的 HTML 骨架(1.html):
<body>
<div class="box-wrap">
<div class="box">
<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>
</body>
我们希望在屏幕正中央放置一个 200×200 的立方体容器。CSS 第一步:
* {
margin: 0;
padding: 0;
}
html, body {
height: 100vh; /* 占满整个视口高度 */
display: flex; /* 开启弹性布局 */
justify-content: center; /* 主轴(默认水平)居中 */
align-items: center; /* 交叉轴(垂直)居中 */
}
🔍 为什么用 100vh 而不是 100%?
vh 和 vw 是 CSS3 引入的 视口单位:
-
100vh = 视口高度的 100%
-
100vw = 视口宽度的 100%
它们把视口等分成 100 份,非常适合移动端适配。而直接写 height: 100% 需要父元素有明确高度才能生效,很容易出现高度塌陷。用 100vh 则一步到位,让 <body> 直接撑满屏幕。
🔍 Flex 居中为何如此简洁?
设置 display: flex 后,<body> 变成了 弹性容器,其直接子元素(.box-wrap)自动成为弹性项目,可以设置宽高,它们的 float、clear、vertical-align 会完全失效。
-
默认 flex-direction: row,主轴为水平方向(从左到右),所有弹性项目会挤在同一行,即使总宽度超出容器也不换行(除非设置 flex-wrap: wrap)。
-
使用 flex-direction: column 可让主轴变为垂直方向,项目堆叠排列。
-
内部的布局规则完全改变:不再遵循普通文档流(块级/行内),而是基于主轴和交叉轴进行排列。
通过:
-
justify-content 控制主轴对齐(默认为水平方向)
-
align-items 控制交叉轴对齐(垂直方向)
我们轻松实现了子元素的水平垂直居中。这是目前最主流的居中方案,比 position + transform 或 table-cell 等方法优雅得多。
二、3D 空间的开启:视距与 3D 渲染上下文
接下来,我们进入 3D 的核心。
.box-wrap {
width: 200px;
height: 200px;
perspective: 600px; /* 3D 视距,景深效果的关键 */
}
.box {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d; /* 让子元素保持 3D 位置 */
animation: rotate 5s linear infinite; /* 旋转动画 */
}
🎯 perspective —— 你的眼睛离屏幕多远?
perspective 定义了观察者与 z=0 平面之间的距离。值越小,透视效果越强烈(近大远小越明显);值越大,透视越平缓。这里设为 600px,给立方体一个适中的景深感。
💡 注意:perspective 要设置在 直接包裹 3D 场景的父元素 上(这里是 .box-wrap),而不是 3D 元素本身,否则透视效果会出错。
🎯 transform-style: preserve-3d —— 让子元素活在 3D 空间
默认情况下,元素的子元素是在 2D 平面 上渲染的,即使父元素发生了 3D 变换,子元素也会被压扁。设置 preserve-3d 后,子元素就能保留各自的 3D 位置,构成立体空间。这正是立方体 6 个面能各朝不同方向的关键。
🎯 动画:让立方体转起来
我们定义了一个简单的关键帧动画:
@keyframes rotate {
0% { transform: rotateY(0deg); }
100% { transform: rotateY(360deg); }
}
这个 @keyframes 定义了名为 rotate 的动画,核心是让元素绕 Y 轴 完成一次 360° 自转。
-
@keyframes rotate
声明一个关键帧动画,名字叫 rotate,供 animation 属性调用(如 .box 上的 animation: rotate 5s linear infinite;)。 -
0% 与 100%
分别代表动画的起点和终点。起点 rotateY(0deg) 为原始朝向,终点 rotateY(360deg) 是绕 Y 轴旋转一整圈后回到原地。 -
rotateY(360deg) 的作用
虽然 360° 和 0° 在静态下看起来一样,但动画会在 0% → 100% 的过程中补齐所有中间角度(如 90°、180° 等),从而产生连续旋转的效果。配合 infinite 无限次播放,立方体就会不停转动。
在这个 3D 立方体项目中,该动画被施加在 .box(包裹六个面的容器)上,因为 .box 设置了 transform-style: preserve-3d,六个面会作为整体一起旋转,最终看到一个完整的立方体绕垂直轴水平旋转。
然后通过 animation: rotate 5s linear infinite; 让 .box 绕 Y 轴匀速无限旋转。目前它还只是个平面方框,下面我们来拼出它的六个面。
-
rotate:动画名称,对应 @keyframes rotate 里定义的从 rotateY(0deg) 到 rotateY(360deg) 的关键帧。
-
5s:一次动画的持续时间,即 5 秒完成一个完整的 360° 旋转。
-
linear:速度曲线,表示匀速旋转,不会出现缓入缓出。
-
infinite:动画重复次数为无限次,所以立方体会一直转下去。
三、拼凑立方体:绝对定位与 3D 位移旋转
每个面都是 .face 类的 <div>:
.face {
width: 200px;
height: 200px;
position: absolute; /* 让所有面重叠于父容器中心 */
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
color: #fff;
opacity: 0.8;
}
关键: position: absolute 使 6 个面全部脱离文档流,并相对于设置了 position: relative 的父容器 .box 定位,默认全部堆叠在左上角。由于宽高相同,它们完全重合。
opacity 的核心特性
-
取值:0(完全透明)到 1(完全不透明)之间的数字,可以带小数(如 0.8)。
-
影响范围:作用于整个元素及其所有子元素(包括文字、背景、边框),且这种透明度是“不可逆”的——子元素无法通过自身的 opacity: 1 恢复不透明。如果需要背景透明但文字不透明,应该用 rgba 或 hsla 来设置背景色,而不是 opacity。
-
层叠上下文:当 opacity 的值小于 1 时,元素会创建一个新的层叠上下文,可能影响 z-index 的层叠关系(但在本立方体中没有用到 z-index,所以不受影响)。
在立方体中的意义
立方体旋转时,半透明的面(opacity: 0.8)让我们能隐约看到背对屏幕的面,增强了立体感和视觉通透性。如果全部不透明(opacity: 1),就只能看到最前面的一个面,立方体旋转的 3D 效果会大打折扣;如果完全透明(opacity: 0),六个面就都消失了。
相对定位(relative)
-
不脱离文档流:元素仍然占据原来的空间,其他元素的位置不会因为它设置 relative 而改变。
-
相对于自身原始位置偏移:通过 top、right、bottom、left 可以移动元素,但原始位置会被保留(留下一个“坑”)。
-
常用作定位参照:我们常给父元素设置 position: relative(但不设置偏移量),这样其内部的绝对定位子元素就会以这个父元素为边界进行定位,而不是更上层的祖先或视口。
.box 本身不需要偏移,它的作用就是为六个 .face 提供一个定位参考系,保证这些面是相对于立方体容器定位,而不是相对于整个页面。
绝对定位(absolute)
-
完全脱离文档流:元素不再占据任何空间,后面的元素会当它不存在,向上填补空位。
-
相对于最近的已定位祖先元素定位:浏览器会向上查找第一个设置了 position: relative/absolute/fixed/sticky 的祖先元素,以它为参考系;如果没找到,则相对于初始包含块(通常是 <html> 或视口)定位。
-
可以精确定位:使用 top、right、bottom、left 将元素锚定在参考系的指定位置。
-
会开启新的上下文
3D 坐标系
想象你的屏幕是一个三维空间:
Y轴 (垂直向下)
│
│
│
└──────────── X轴 (水平向右)
/
/
/
Z轴 (垂直屏幕,指向你的眼睛为正方向)
-
X 轴:水平向右(translateX、rotateX 都基于此)
-
Y 轴:垂直向下(translateY、rotateY 都基于此)
-
Z 轴:从屏幕内部指向外部,正方向朝向你的眼睛(translateZ、rotateZ 都基于此)
注意:rotateX 是绕着 X 轴旋转,而不是沿 X 轴移动。顺时针方向从轴的正方向看向原点。
二、translate:沿坐标轴移动
translate 只移动元素,不改变它的朝向。在立方体中,我们用它把六个面从中心点推到正确的位置。
| translateX(100px) | 沿 X 轴向右移动 100px | 面从中心向右跑 |
| translateY(-100px) | 沿 Y 轴向上移动 100px(因为 Y 轴正方向向下,负值向上) | 面从中心向上跑 |
| translateZ(100px) | 沿 Z 轴向屏幕外移动 100px(离你更近) | 面离你更近 |
图解立方体前面的位移:
+——–+ Z=0平面 +——–+
| 前面 | ← 原本在中心(0,0,0) | 前面 | (0,0,100)
+——–+ +——–+
离你近了100px
代码:.front { transform: translateZ(100px); }
三、rotate:绕坐标轴旋转
rotate 让元素绕指定轴旋转,轴本身不动,元素围着轴转。正角度方向遵循“左手定则”或“顺时针从轴正方向看”。
| rotateX(90deg) | 绕 X 轴 | 上边缘向后倒,下边缘向你翻来(像翻牌) |
| rotateY(90deg) | 绕 Y 轴 | 右边缘向后转,左边缘向你转来(像开门) |
| rotateZ(90deg) | 绕 Z 轴 | 在屏幕平面内顺时针旋转(普通2D旋转) |
图解 rotateY(90deg) 对右面的作用:
初始(朝向你) 绕 Y 轴转 90°
+——–+ +——–+
| 右面 | (朝向Z) → || 右面 || 现在朝右,你从正面看到的是它的侧面
+——–+ +——–+
Z轴方向 X轴正方向
代码:.right { transform: translateX(100px) rotateY(90deg); }
先平移再旋转:将面推到右侧(X=100),然后绕 Y 轴旋转 90°,使面朝向右侧。
四、组合变换与顺序陷阱
所有变换函数写在一个 transform 属性里时,执行顺序是从右向左,也就是最后的函数最先执行。
transform: translateX(100px) rotateY(90deg);
/* 先旋转,再平移 */
对于右面,我们通常希望:先平移到右边,再旋转让它朝向右边。但由于函数从右向左执行,如果写成上面那样,实际是先旋转 90°(面朝向右边),然后沿着 X 轴平移 100px。但因为面已朝向右边,它的 X 轴仍然是全局的 X 轴(水平方向),所以它仍然能到达正确位置,只是顺序反了也可能得出同样结果。但有时顺序不同会导致完全不同的位置,所以更好的是先平移再旋转,逻辑更直观,
接下来,我们用 transform 把它们挪到正确的位置并旋转到正确的朝向。
📐 前后两面:沿 Z 轴平移
.front { transform: translateZ(100px); } /* 向屏幕外移动半宽 */
.back { transform: translateZ(-100px) rotateY(180deg); } /* 向屏幕内移动,并翻转 */
立方体边长为 200px,所以前后两面各沿 Z 轴移动 ±100px。后面的面需要 rotateY(180deg) 这样它的文字才是正的(否则从后面看是反的)。
📐 左右两面:沿 X 轴平移 + 绕 Y 轴旋转
transform: translateX(100px) rotateY(90deg);
/* 先旋转,再平移 */
先平移到左右两侧,再绕 Y 轴旋转 90 度,使面朝向左侧或右侧。
📐 上下两面:沿 Y 轴平移 + 绕 X 轴旋转
这是最容易出错的地方。笔记中的代码写成 translateX,显然是笔误。正确写法:
.top { transform: translateY(-100px) rotateX(90deg); }
.bottom { transform: translateY(100px) rotateX(-90deg); }
或者保持 rotateX(90deg) 配合 translateZ 等方案,但最常见的组合就是:上下移动用 translateY,抬头低头用 rotateX。
🔧 注意变换顺序:translate 和 rotate 的执行顺序是从右向左,即先旋转再平移。但在这里,只要最终能到达正确位置即可。如果不确定,推荐先平移后旋转,逻辑更直观。
至此,6 个面就位,一个完整的 3D 立方体诞生,并随着父容器的 rotateY 动画流畅旋转。
效果:
.box {
display: inline-block;
width: 50%;
background-color: red;
}
四、插曲:行内、块级与那个经典的空格坑
在 2.html 中,我们研究了一个常见问题:
<div class="box">1</div>
<div class="box">2</div>
📦 块级元素 (block)
-
可以设置宽高
-
独占一行,会把后面的元素挤到下一行
📦 行内元素 (inline)
-
无法设置宽高(由内容撑开)
-
不会独占一行,与其他行内元素并排
📦 行内块级 (inline-block)
通过 display: inline-block 可以让元素:
-
既可以设置宽高(块级的优点)
-
又不会独占一行(行内的优点)
看起来是完美结合,但有个 天坑:HTML 中元素间的换行符、空格会被渲染成一个 空格字符,占据一定宽度。
比如:
<div class="box">1</div>
<div class="box">2</div>
两个 div 之间的换行会产生一个看不见的空格,导致 50% + 50% + 空格 > 100%,第二个 div 被挤到下一行。
🛠 解决方案:
消除 HTML 间的空隙:把标签紧挨着写
<div class="box">1</div><div class="box">2</div>
父元素设置 font-size: 0,再在子元素上恢复字号
使用 Flex 或 Grid 布局 替代 inline-block(更现代的做法)
2.html 中正是用第一种方法解决了空格问题,让两个 50% 宽度的盒子完美并排。
五、Flex 的弹性世界:flex:1 到底做了什么?
3.html 展示了 Flex 布局的另一个常见用法:
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
</div>
.box {
display: flex;
}
.item {
flex: 1;
background-color: red;
text-align: center;
}
.box 设置了 display: flex,变为弹性容器,其子元素自动成为 弹性项目。默认主轴为水平方向 (flex-direction: row)。
🧩 flex: 1 的真相
flex: 1 是 flex: 1 1 0% 的简写,即:
-
flex-grow: 1 —— 有剩余空间时,项目会按比例放大
-
flex-shrink: 1 —— 空间不足时,项目会按比例缩小
-
flex-basis: 0% —— 项目在分配多余空间之前的初始大小为 0
这样一来,所有 .item 会 平均瓜分 容器的宽度,无论容器大小如何变化,它们始终等宽。这种弹性特性让 Flex 成为移动端布局的首选,完全不需要计算百分比或使用 calc()。
如果想切换为纵向排列,只需在容器上加一句 flex-direction: column,这时主轴变为垂直方向,flex: 1 就会让项目均分高度。
六、回看 common.css:属性间的默契配合
综合回顾整个 3D 立方体的 CSS(common.css),每一行代码都不是孤立的:
* { margin:0; padding:0; }
清除默认内外边距,避免不必要的滚动条和位移,是全局重置的第一步。
html, body { height: 100vh; display: flex; justify-content: center; align-items: center; }
让 body 撑满视口并成为弹性容器,使得 .box-wrap 完美居中。这里的 vh 单位保证了高度永远等于屏幕可视高度,避免了百分比高度的依赖问题。
.box-wrap { perspective: 600px; }
在包裹 3D 场景的容器上设置视距,创造景深感。如果放在 .box 上,透视中心会随着旋转而移动,产生怪异效果。
.box { position: relative; transform-style: preserve-3d; }
-
position: relative 是给内部的绝对定位面片提供定位参考(虽然 .face 是相对于最近的定位祖先定位,但如果 .box 没有定位,面会相对于 body 定位,导致位移基准错误)。
-
preserve-3d 是 3D 空间的核心开关,没有它,6 个面的 3D 变换会被压扁在一个平面内。
.face { position: absolute; }
让所有面脱离文档流并叠在一起,通过各自的 transform 去往不同位置。opacity: 0.8 让旋转时能略微看到背后的面,增强立体感。
动画
动画绑定在 .box 上,让它绕 Y 轴旋转。由于 6 个面都是 .box 的子元素且处于 3D 空间中,它们会作为一个整体旋转,形成立方体旋转的视觉效果。
每一个属性都精准地服务于最终的 3D 呈现,彼此之间的依赖关系清晰而紧密:视距 → 3D 渲染上下文 → 绝对定位重叠 → 位移旋转分散 → 父容器动画整合。任何一环缺失,立方体都会崩塌。
结语
从一个 3D 立方体出发,我们串联了:
-
视口单位 vh/vw 与移动端适配
-
Flex 布局的居中与弹性分配
-
块级、行内、行内块级的特性与经典空格 Bug
-
CSS 3D 变换的完整构建流程
这些知识点看似零散,却都在实际项目中高频出现。把它们揉进一个有趣的 Demo 里,既能加深理解,也方便日后查阅。
不妨动手把代码复制下来跑一遍,再尝试修改视距、改变边长、增加面片,看看效果如何变化。CSS 3D 的世界远比想象中好玩得多。
本文到此结束,如果对你有帮助,欢迎点赞收藏,也欢迎在评论区交流你的 3D 玩法!





