CSS 3D:从布局到立方体

CSS 3D:从布局到立方体

前端页面看似是 2D 的,但浏览器里始终存在一个隐形的 z 轴。

这节课从最基础的"布局"出发,先把外层盒子与内层盒子的分工、水平垂直居中、行内与块级元素、定位这些容易被忽视的基本功补齐,再一步步把它们组合成 CSS 3D 的核心能力------perspectivetransform-styletranslate3drotate3d,最终落地为一个会自动旋转的六面立方体。文章里我会把我自学的笔记重新整理一遍,也补一些当时容易混的点。

为什么前端要聊 3D

在接触进行这方面的学习之前,我对 CSS 的理解基本停留在"搭盒子、调样式"。但实际上,CSS 不仅能做 2D 布局,还能通过一组 3D 属性直接在浏览器里画立体效果。

CSS 3D 不止于"做出 3D 效果"这一件事,更关键的是它会带来 GPU 加速 。哪怕是 2D 的界面,有时我们也会手动把它 3D 化(比如 translateZ(0)),目的就是触发 GPU 硬件加速,让动画更顺滑。这一点和之前提到的 Canvas 里 getContext('webgl') 调用 GPU 能力是同一种思路:浏览器里凡是要画东西,最终都要看显卡给不给力

正式进入 3D 之前,我们需要花不少时间补布局基础。这是相当有必要的------CSS 3D 的本质是"在布局好的盒子上叠加空间变换",如果布局没搞清楚,3D 的变换就无从谈起。

布局:外层负责布局,内层负责内容

这将是这节课我反复强调的一句话:

外层盒子负责布局,里面的盒子负责做内容。

听起来像废话,但它在实际开发里是一个非常重要的拆分原则。比如下面这个 3D 立方体的结构:

html 复制代码
<!-- 外层盒子负责布局,里面的盒子负责做内容 -->
<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>
  • .box-wrap 是外层,决定"立方体放在页面哪里";
  • .box 是中层,决定"立方体本身怎么旋转";
  • .face 是内层,决定"每一面长什么样"。

这种"布局与内容分离"的思路,后面会一再出现。写 CSS 时如果不分开,很容易出现"改一处样式把整个结构都搞乱"的情况。

水平垂直居中:先有视口,再谈居中

布局里最常见的需求就是"水平垂直居中"。这节课从最基础的视口单位讲起。

vh / vw:视口单位

让一个元素铺满全屏,最直接的做法是:

css 复制代码
html, body {
    width: 100%;       /* 块级元素宽度默认 100% */
    height: 100vh;     /* CSS3 新增的视口单位 */
}

vh 是 viewport-height,vw 是 viewport-width。它们把整个屏幕(PC 端、移动端等)等比例分成 100 份,以此来达到移动端适配。

但移动端有一个坑:在 Safari 等浏览器上,100vh 有时会包含地址栏和工具栏的高度,导致元素超出预期。这时候可以考虑使用 100dvh(动态视口高度)作为更精准的替代方案------地址栏滑出时高度会自动调整。

flex 实现水平垂直居中

有了全屏视口之后,居中就交给 flex:

css 复制代码
html, body {
    display: flex;
    flex-direction: column;   /* 主轴方向,剩下的就是次轴 */
    justify-content: center;  /* 主轴对齐 */
    align-items: center;      /* 次轴对齐 */
}

三个要点我记了下来:

  1. display: flex 会在当前盒子里开启一个弹性格式化上下文
  2. flex-direction 决定主轴方向,剩下的方向就是次轴;
  3. justify-content 管主轴,align-items 管次轴。

flex 是移动端视窗大小多变情况下最常用的布局方案。这节课后面所有的居中,几乎都是这种"父容器 flex + 子元素居中"的模式。

行内 / 块级元素:display 属性的本质

布局搞清楚之后,下一个容易混的点是 display 属性。HTML 元素本身分两类:

  • 块级元素divul 等)
    • display 默认是 block
    • 独占一行
    • 可以设置宽高
    • 用来做盒子
  • 行内元素span 等)
    • display 默认是 inline
    • 不独占一行
    • 不可以设置宽高
    • 用来做文字、超链接、图片等

浏览器会给一些元素默认的 display 行为,但我们可以通过 display 手动切换 inline / block,把块级元素改成行内元素,或者反过来。

flex:父与子的布局关系

display: flex 时,会在当前盒子(也就是 flex 容器)内开启一个弹性格式化上下文。弹性布局是父与子之间的布局关系,子元素默认会沿主轴对齐,被父元素限制着。

示例 3.html 把这点体现得很清楚:

html 复制代码
<div class="box">
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
</div>
css 复制代码
.box {
    /* 弹性布局的子元素们,默认会主轴对齐,被父元素限制着
       弹性布局是父与子之间的布局关系
       开启了格式化上下文 */
    display: flex;
}
.item {
    flex: 1;
    background-color: #9c1818;
    width: 50%;
    text-align: center;
}

四个 .item 各自 flex: 1,会平均分配主轴空间,无论 width: 50% 写成什么。这就是 flex 的"父管布局"特性。

inline-block 的天坑:空白字符间隙

display: inline-block 是一个介于行内和块级之间的属性值:

  • 不独占一行
  • 同时可以设置宽高

但它有一个经典的坑------默认空格符会占据一定的大小 。HTML 源码里的 \n\r、空格,都会被浏览器渲染成一个空白字符,导致两个 50% 宽度的盒子加起来超过 100%,第二个盒子被挤到下一行。

2.html 就是这个坑的复现:

html 复制代码
<div class="box">1</div>
<div class="box">2</div>
css 复制代码
.box {
    background-color: #9c1818;
    display: inline-block;
    width: 50%;
    /* 由于 HTML 源码中 <div> 标签之间有换行和空格,浏览器会渲染出一个空白字符间隙。
       结果是两个 50% 宽度的盒子 + 间隙 > 100%,第二个盒子会被挤到下一行。 */
}

解决方法有几种:

  • 把 HTML 标签紧挨着写,不留空白;
  • 父元素设置 font-size: 0,子元素再重新设置字号;
  • 干脆用 flex,避开 inline-block 的这个坑。

实际开发里,能用 flex 就别用 inline-block 做布局,这是这节课我得到的最大提醒之一。

定位:relative 与 absolute

讲完 display,下一个基础是定位。CSS 3D 里六面立方体的每个面都要用绝对定位叠加在一起,所以这个点必须先理清。

css 复制代码
position: relative;  /* 相对定位 */
position: absolute;  /* 绝对定位 */
  • relative:相对自己原来的位置偏移,仍然占据文档流
  • absolute:脱离文档流,相对于最近的非 static 定位祖先元素偏移。

在立方体的例子里,.boxposition: relative,作为定位上下文;六个 .face 都是 position: absolute,全部叠加在 .box 的左上角,然后通过 translate 把它们各自挪到对应的方向。这是"外层 relative + 内层 absolute"的经典组合。

CSS 3D 核心:perspective 与 transform-style

布局基础补完,正式进入 3D。CSS 3D 的核心其实只有两个属性。

perspective:视距

perspective 定义了观察者到 z=0 平面的距离,单位是 px。它决定 3D 效果的"透视强度"------值越小,透视越夸张(近大远小越明显);值越大,越接近正交投影。

css 复制代码
.box-wrap {
    width: 200px;
    height: 200px;
    perspective: 600px;  /* 3D 核心:视距 */
}

注意 perspective 要写在需要被透视的元素的父元素上,而不是元素本身。这是一个非常容易踩的坑。

transform-style: preserve-3d

光有 perspective 还不够。默认情况下,子元素是被"压平"在父元素平面上的,要做 3D 立方体,必须让父元素保留子元素的 3D 空间:

css 复制代码
.box {
    width: 200px;
    height: 200px;
    position: relative;
    transform-style: preserve-3d;  /* 保留子元素的 3D 空间 */
    animation: rotate 6s linear infinite;
}

transform-style: preserve-3d 这一句是 3D 立方体的关键。没有它,六个面会被压成一个平面,怎么 translateZ 都没用。

六面立方体:translate + rotate 的组合

理解了上面两个核心属性,立方体就是一道几何题。200×200 的立方体,每个面都要从原点(左上角)挪到对应的方位。

先把每个面叠在原点

六个面共用一组基础样式,先用绝对定位把它们叠在 .box 的左上角:

css 复制代码
.face {
    width: 200px;
    height: 200px;
    left: -50px;   /* (100 - 200) / 2,让面相对外层 100x100 居中 */
    top: -50px;
    position: absolute;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 30px;
    color: #b41c1c;
    opacity: 0.8;
}

这里 left: -50pxtop: -50px 是为了把 200×200 的面,相对外层 100×100 的容器做一次居中偏移((100 - 200) / 2 = -50)。

沿三根轴把每个面推出去

立方体六个面,对应三根轴的正负方向。每个面都是"先沿轴平移 100px,再旋转到对应朝向":

css 复制代码
.front {
    background: #429911;
    transform: translateZ(100px);   /* 朝前,沿 z 轴正方向 */
}
.back {
    background: #114299;
    transform: translateZ(-100px) rotateY(180deg);  /* 朝后,先退后,再翻转 */
}
.left {
    background: #994211;
    transform: translateX(-100px) rotateY(-90deg);  /* 逆时针为负 */
}
.right {
    background: #429911;
    transform: translateX(100px) rotateY(90deg);    /* 顺时针为正 */
}
.top {
    background: #994211;
    transform: translateY(-100px) rotateX(90deg);
}
.bottom {
    background: #429911;
    transform: translateY(100px) rotateX(-90deg);
}

这里有一个我反复记错的点:旋转方向

  • rotateY(90deg):绕 Y 轴顺时针旋转 90 度(从 +Y 轴往原点看);
  • rotateY(-90deg):逆时针。

为什么 left 面要先 translateX(-100px)rotateY(-90deg)?因为先旋转再平移,平移方向会跟着旋转矩阵变,容易算错。先平移到位置,再旋转朝向,是更不容易出错的处理顺序。

顺序很重要:translate 在前,rotate 在后

CSS 的 transform 是从右往左执行的,但写在一起时,习惯上把"想先做的"写在右边。所以:

css 复制代码
transform: translateX(-100px) rotateY(-90deg);

实际执行顺序是:先 rotateY(-90deg) 把面转到朝左,再 translateX(-100px) 沿旋转后的 x 轴 推出 100px。这听起来和上面"先平移再旋转"矛盾,但其实不矛盾------CSS 里 translateXtransform 字符串里写在前面,意味着它作用于"已经被后面 rotate 过的坐标系"。这里我建议把它当成一个约定来记:

立方体六面公式:translate{轴}(±100px) rotate{另一轴}(±90deg),平移在前,旋转在后。

记住这个公式,六个面都能直接写出来。

旋转动画:@keyframes

立方体做完之后,最后一步是让它转起来。CSS 动画的核心是 @keyframes + animation 属性。

css 复制代码
.box {
    animation: rotate 6s linear infinite;
}

@keyframes rotate {
    0%   { transform: rotateX(0deg)   rotateY(0deg); }
    25%  { transform: rotateX(0deg)   rotateY(90deg); }
    50%  { transform: rotateX(0deg)   rotateY(180deg); }
    75%  { transform: rotateX(0deg)   rotateY(270deg); }
    100% { transform: rotateX(360deg) rotateY(360deg); }
}

animation 是一个简写属性,包含四个关键信息:

  • 动画名称 (自定义,相当于动作导演):rotate
  • 动画时间 duration (一次动画持续时间):6s
  • 动画曲线 (变化的速率):linear,匀速
  • 无限循环 (是否重复播放):infinite

@keyframes 定义动画的关键帧。这里前 75% 只绕 Y 轴转,最后 25% 才加上 X 轴翻转,整体看起来像"先转一圈看四面,再翻一下看顶底"。

注意 transform 写在动画里时,每一帧都是完整的变换 ,不是增量。也就是说 50% 那一帧的 rotateY(180deg) 不是"在 25% 的基础上再加 90 度",而是直接 rotateY(180deg)。这点和 Canvas 里"每帧 += speed"的增量式动画很不一样。

课后复习:几个容易混的点

把课堂里几个关键问题整理在这里。

1. perspective 写在谁身上

text 复制代码
写在"被透视元素的父元素"上。
写在自己身上不生效,写在更远的祖先上会失效。

2. transform-style 为什么必须有

text 复制代码
默认 transform-style 是 flat,子元素会被压平在父元素平面上。
preserve-3d 才会保留子元素的 3D 空间,让六个面真正立体堆叠。

3. 立方体六面公式

text 复制代码
front  : translateZ( d)
back   : translateZ(-d) rotateY(180deg)
left   : translateX(-d) rotateY(-90deg)
right  : translateX( d) rotateY( 90deg)
top    : translateY(-d) rotateX( 90deg)
bottom : translateY( d) rotateX(-90deg)

其中 d 是面到中心的距离,对 200×200 的立方体来说 d = 100

4. 自测题

  1. vhvw 分别表示什么?为什么移动端推荐用 dvh
  2. inline-block 的"空白间隙"是怎么产生的?怎么解决?
  3. perspective 应该写在立方体本身,还是它的父元素上?为什么?
  4. transform-style: preserve-3d 如果不写,立方体会变成什么样?
  5. 立方体的 left 面为什么是 translateX(-100px) rotateY(-90deg),而不是反过来?

现在如何理解

写完这篇文章,我对 CSS 的"画"能力有了新的认识。

CSS 3D 不是一门新语言,而是把"布局 + 定位 + 变换"这三件事叠加起来。perspective 提供视距、preserve-3d 保留空间、translate3d / rotate3d 做变换------三个属性就把一个 2D 盒子变成了六面立方体。但要让这个立方体真的"对",就必须把布局基础打牢:外层布局、内层内容的拆分,display 属性的本质,relative + absolute 的组合,flex 居中的逻辑。3D 是建立在 2D 布局之上的,没有 2D 的基础,3D 就是空中楼阁。

另外,这节课也让我再次意识到 GPU 加速的意义。CSS 3D 不只是为了"炫",更是为了"快"------translateZ(0) 这种看起来无意义的变换,背后其实是在主动调用显卡。这一点和上一节 Canvas 里的 getContext('webgl') 是同一种思路:浏览器里凡是要画东西,最终都要看显卡给不给力。

inline-block 的空白间隙、100vh 在移动端的坑、perspective 写错位置不生效------这些都是课堂里看起来不起眼的小点,但每一个都是真实开发里会反复踩的坑。我把它们和六面公式一起记下来,以后写 3D 之前先看一遍,能少走不少弯路。

后面如果要继续往 3D 方向走,自然的延伸是 three.js------上一节 Canvas 课里也提到过,AI 游戏、3D 可视化方向它都是重要线索。CSS 3D 适合做卡片翻转、轮播、轻量动效,真正复杂的 3D 场景还是要交给 WebGL。但无论走哪条路,这节课里"布局是 3D 的地基"这个认知,都会一直适用。

相关推荐
梨子同志1 小时前
React
前端
万少1 小时前
22 点后,我靠这个 AI 工具成了"夜间天才程序员"
前端·后端
狂师2 小时前
比 Playwright 更给力,推荐一个AI Agent的浏览器自动化开源项目!
前端·开源·测试
IT_陈寒2 小时前
React hooks 闭包陷阱把我的状态吃掉了,原来问题出在这里
前端·人工智能·后端
壹方秘境2 小时前
使用ApiCatcher在 iOS 上像修改 hosts 一样自定义域名解析
前端·后端·客户端
柳杉2 小时前
可视化大屏设计器脚手架:从设计到交付的一站式方案
前端·three.js·数据可视化
kyriewen15 小时前
我手写了一个 EventEmitter,面试官追问了 6 个问题——第 4 个我没答上来
前端·javascript·面试
IT_陈寒16 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
小林攻城狮16 小时前
使用 Transport 节流解决 Vercel AI SDK 流式渲染卡死问题
前端·react.js