本章目录 / 案例进度
第 3 章 · 现代布局技术
Flexbox 与 Grid:分工、原理与选型
这两个模块不是竞争关系,而是分工关系。理解「一维 / 二维」这一条分界线,你就能在写第一行代码之前决定用谁 —— 而不是先写 Flex,发现不行再换成 Grid。
案例第 3 阶段:把骨架写成代码。骨架方案定了:桌面三栏(左归档 / 中正文 / 右信息)、平板两栏、手机单栏。本章的任务是选对工具 —— 页面骨架用 Grid,卡片内部用 Flex,并让这套分工在整份代码里保持一致。
Flexbox:一条主轴上的分配问题
Flex 只解决一件事:让一组项目在一条轴上排布,并决定剩余空间怎么分。
讲解
主轴与交叉轴
Flex 容器里有两个轴:主轴(main axis,由 flex-direction 决定)与交叉轴(cross axis,垂直于主轴)。所有属性都只作用在其中一个轴上,把这条记牢,属性名就不再需要死记:
justify-content→ 主轴上的分布(项目之间和两端怎么分空间)align-items→ 交叉轴上的对齐(单行内每个项目怎么对齐)align-content→ 交叉轴上「行之间」的分布(只在换行且有多行时有效)
方向改变时,两个轴的角色会互换 —— flex-direction: column 之后,justify-content 管的是垂直方向。这也是 Flex 最常被误用的地方:以为属性写错了,其实是轴换了。
容器决定「怎么排」,项目决定「占多少」
项目侧最核心的是三个属性,它们共同回答「这一项占用多少空间」:
flex-grow:有剩余空间时,按比例分给谁(默认 0,即不参与分配)flex-shrink:空间不够时,谁可以被压缩(默认 1)flex-basis:分配前的基础尺寸(默认auto,即由内容决定)
日常用得最多的是简写 flex: 1 1 0(等分剩余空间)与 flex: 0 0 200px(固定 200px 不接受压缩)。注意 flex: 1 展开后是 1 1 0%,其中的 basis: 0 意味着「忽略内容宽度,从零开始等分」—— 这是 flex: 1 与 flex: auto(= 1 1 auto,内容宽的项会更宽)的关键差别。
两个高频陷阱。一是 min-width: auto:Flex 项目默认不会缩到比内容更窄,因此一段长文本或长代码会把容器撑破 —— 给它加 min-width: 0 即可。二是嵌套过深:用 Flex 套 Flex 去模拟二维表格,代码会迅速失控,那正是 Grid 的活。
- Flex 的强项
- 导航条、按钮组、表单行、卡片内部(图 + 文)、底部对齐的卡片页脚 —— 凡是「一排东西」都是 Flex 的主场。
- Flex 的边界
- 它不知道「列」的存在:多个容器之间无法共享轨道,因此跨卡片对齐标题、跨行对齐高度都要靠别的办法。
关键代码
.post-card {
display: flex;
flex-direction: column; /* 主轴改为垂直:卡片上下堆叠 */
gap: 12px;
min-width: 0; /* 允许被压缩,防止长内容撑破容器 */
}
.post-card__meta {
display: flex;
align-items: center; /* 交叉轴(垂直)居中 */
justify-content: space-between; /* 主轴(水平)两端分布 */
gap: 8px;
}
.post-card__footer {
margin-top: auto; /* auto margin 吸走剩余空间 → 页脚被压到底部 */
}
.post-card__title {
flex: 1 1 0; /* grow shrink basis:等分可用宽度 */
min-width: 0; /* 配合 overflow:hidden 才能让长标题出现省略号 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
margin-top: auto是 Flex 里最实用的技巧之一:它把「剩下的空间」全吃掉,于是页脚被顶到底部,而不需要position: absolute或固定高度。- 注意
min-width: 0出现了两次:一次在容器上(允许自己被压缩),一次在标题上(允许省略号生效)。这是 Flex 最常见的一类「玄学问题」的真正原因。 - 用
gap而不是margin-right:不必处理最后一个元素,间距也不会在压缩时被吃掉。
动态演示
推荐路径:先把 flex-wrap 改成 wrap 并把容器高度拉到 300,然后逐个试 align-content —— 你会看到「单行对齐」和「多行分布」的差别。再切到 column,观察两个轴的角色互换、以及轴标签的移动。
Grid:先画轨道,再放内容
Grid 的工作方式与 Flex 相反:先在容器上定义好行列轨道,再把项目放进格子里。
讲解
轨道、单元格、区域
Grid 把容器切成轨道(track,即行与列),轨道交叉形成单元格,相邻单元格可以合成区域。项目通过三种方式落位:自动放置(默认,按顺序填)、线号定位(grid-column: 2 / 4)、命名区域(grid-area: main)。
关键词是「先声明,后放置」。这让你在写内容之前就先描述版式,也正是它能替代大量断点与嵌套的原因。
fr 与 minmax:弹性轨道的两种写法
1fr表示「剩余空间的一份」。1fr 2fr 1fr就是三列按 1:2:1 分配剩余空间。minmax(0, 1fr)与1fr的差别很关键:轨道的最小值是auto时,内容(长单词、长代码、表格)会把轨道撑得比 1fr 更宽,导致整个网格溢出。只要列里有不可预测的长内容,就写minmax(0, 1fr)。minmax(200px, 1fr)混用固定与弹性:侧栏恒为 200px,主内容吃掉剩下的。minmax(0, 220px)则让侧栏「最多 220px,窄了就缩」。
auto-fill / auto-fit:不写媒体查询的自适应
repeat(auto-fill, minmax(220px, 1fr)) 是 Grid 最有价值的一行代码:容器变宽就自动多塞几列,变窄就自动减少列数,完全不需要媒体查询。它有两点需要注意:
auto-fill会保留空轨道,auto-fit会把空轨道折叠掉。元素少、希望它们「撑满一行」时用auto-fit;希望保持与别处一致的列宽节奏时用auto-fill。minmax()里的最小值决定了「多少像素开始换列」—— 它就是断点的替代物,只是由浏览器替你算。
显式网格与隐式网格。你声明的行叫显式网格;项目超出的部分会由浏览器自动生成隐式网格来容纳。grid-auto-rows: minmax(120px, auto) 可以控制这些自动行的最小高度,是「卡片墙」保持等高节奏的常用手法。
关键代码
/* 1. 等分:三列同宽 */
.tracks--even {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
/* 2. 固定 + 弹性:侧栏恒宽,主内容吃掉剩余空间 */
.tracks--sidebar {
display: grid;
grid-template-columns: minmax(0, 1fr) 240px;
gap: 24px;
}
/* 3. 自适应卡片墙:不写媒体查询,容器变宽自动加列 */
.tracks--auto {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
grid-auto-rows: minmax(120px, auto); /* 隐式行也有最小高度 */
}
/* 显式放置:让某一项跨列、跨行 */
.featured {
grid-column: 1 / -1; /* 从第 1 条线到最后一 条线 = 通栏 */
grid-row: span 2; /* 占两行,通常用于「一主多辅」的卡片墙 */
}
1 / -1里的-1表示「最后一条列线」,因此它天然是「通栏」,无论栅格有多少列 —— 比写1 / 13更安全。- 自适应卡片墙的代价是列宽不完全可控(有 ±220px 内的浮动),做像素级对齐的版面时仍然要用断点或命名区域。
- 本站的演示区、卡片阵列、货架式布局大量使用了
repeat(auto-fit, minmax(...)):它是「少写断点」的主要手段。
动态演示
试三次:① 切到 repeat(auto-fill, …) 并拖动浏览器窗口,看列数自己变化;② 切到 1fr 2fr 1fr 再点 justify-items 的 start/center,看项目在格子内移动;③ 行模板设为 repeat(2, …) 并把项目加到 6 个,看隐式行如何接住多余的项目。
命名区域:把版式画出来
grid-template-areas 让你用 ASCII 图直接描述页面结构 —— 这大概是 CSS 里最接近「所见即所得」的一处。
讲解
用字符画出结构
每个字符串是一行,字符串里的每个名字是一个单元格,同名的相邻单元格合成一个区域,. 表示空格子。下面这张「图」就是一份可执行的版式声明:
grid-template-columns: 180px minmax(0, 1fr) 240px;
grid-template-areas:
"header header header"
"rail main side"
"footer footer footer";
它有两个别的手法很难替代的优点:① 可读性 —— 半年后再看这八行,你仍然能立刻说出页面长什么样;② 与 DOM 顺序解耦 —— HTML 里可以按内容优先级写(正文在前),视觉位置由这张图决定。
一条硬性规则:区域必须是矩形
CSS 规定:同名区域必须构成一个矩形。如果你把 a 画成 L 形或断开的两块,整条 grid-template-areas 声明会被整体丢弃(不是局部失效),所有项目退回到自动放置 —— 页面看起来会「莫名其妙地散架」。这是使用命名区域时唯一的重大陷阱,而好消息是它很容易用肉眼检查:同名的格子连起来必须是个方块。
配合断点重排,只需重写这张图。响应式改造时不必动 HTML、也不必重写一堆属性 —— 在 @media 里把 areas 换成单列版本即可(本站案例正是这么做的,见第 4 章与第 5 章)。
- 什么时候值得用
- 页面级、有名字的区域(页头 / 导航 / 正文 / 侧栏 / 页脚)—— 区域越「大」、越稳定,命名区域越划算。
- 什么时候不必用
- 同构的卡片阵列(每张卡都一样)用
repeat(auto-fill, …)更省事;此时给每张卡起名字反而是噪声。
关键代码
.page {
display: grid;
grid-template-columns: 180px minmax(0, 1fr) 240px;
grid-template-rows: auto minmax(0, 1fr) auto;
/* 这三行字符串就是版式本身 */
grid-template-areas:
"header header header"
"rail main side"
"footer footer footer";
gap: 24px;
min-height: 100dvh; /* dvh 而不是 vh:移动端地址栏收放不会导致跳动 */
}
.page > header { grid-area: header; }
.page > nav { grid-area: rail; }
.page > main { grid-area: main; min-width: 0; }
.page > aside { grid-area: side; }
.page > footer { grid-area: footer; }
/* 断点里只重画这张图,HTML 一行都不用改 */
@media (max-width: 900px) {
.page {
grid-template-columns: minmax(0, 1fr);
grid-template-areas:
"header"
"main"
"rail"
"side"
"footer";
}
}
- 字符串之间的空白无关紧要,但每个字符串的字符数必须一致(列数相同),否则声明同样会失效。
- 用
.可以留出真正的空位,例如". main side"让第一列在那一行完全空着。 - 断点里的重排顺序就是移动端的阅读顺序:把
main提到rail之前,读者先看到正文。
动态演示
点击格子涂色(当前画笔见下方);a / b / c 分别代表页头 / 正文 / 侧栏。
这是浏览器按你生成的 grid-template-areas 真实渲染的结果:
故意画一个「断开的 a」(例如第一行涂 a a a、第三行中间再涂一个 a):状态会立刻变成「区域非法」,右边预览也会散架 —— 这就是 CSS 丢弃整条声明的结果。再清空重画成两个规整的矩形,恢复正常。
选型:三个问题定下用谁
不要在写完 Flex 之后才发现需要 Grid。选型可以压缩成三个是非问题。
讲解
一条主判据:维度
一维用 Flex,二维用 Grid。「维度」不是指「看起来有几行几列」,而是指你是否需要同时控制行与列。导航条是一排项目 —— 一维,Flex。卡片墙要让每张卡片的标题、正文、按钮在横向与纵向都对齐 —— 二维,Grid(内部配合 Flex)。
两条辅助判据
- 尺寸由谁决定?内容驱动(项目多宽就多宽,剩余空间再分配)→ Flex。
flex-basis: auto的默认行为就是内容优先。布局驱动(版面先定,内容去适应格子)→ Grid。 - 需要跨容器对齐吗?同一行里的多张卡片,标题必须落在同一条基线上 —— 这要求它们共享行轨道,只有 Grid 能做到。Flex 里的项目彼此独立,无法互相「对齐到同一行」。
它们最常见的合作方式:Grid 搭骨架,Flex 做零件
真实项目里两者几乎总是一起出现,分工非常清晰:页面级、区域级用 Grid(页头 / 正文 / 侧栏 / 卡片阵列),组件内部用 Flex(按钮组、标签行、卡片的图 + 文、页脚的一排链接)。本站就是这套分工:整站骨架与卡片阵列是 Grid,顶栏、控件行、卡片内部是 Flex。
反过来说也有一条经验:如果你发现自己在 Flex 容器里嵌套了三层 Flex 只为了对齐网格,那几乎一定是选错了工具;如果你为了让 Grid 里的项目「按内容宽度排列」而反复调 auto-fit 和 justify-content,那也可能是该用 Flex。
关键代码
/* A. Flex 版:项目各自独立,宽度靠 flex-basis 约定 */
.cards--flex {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.cards--flex > .card {
flex: 1 1 220px; /* 每张卡至少 220px,剩余空间平分 */
}
/* ⚠️ 最后一行不足三张时,卡片会被拉宽填满整行 —— 有时这正是你要的,
但若要保持列宽一致,就需要再加一层占位元素或改用 Grid */
/* B. Grid 版:先定轨道,卡片只负责占位 */
.cards--grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
/* 每个格子宽度由轨道决定,因此同一行的卡片天然等宽;
卡片内部的标题 / 正文 / 按钮再用 Flex 排布(见 3.1) */
.cards--grid .card {
display: flex;
flex-direction: column;
gap: 8px;
}
.cards--grid .card__cta { margin-top: auto; } /* 按钮统一吸到底部 */
- 两种写法都能跑,差别在于「列宽由谁保证」:Flex 版靠每个项目自己,Grid 版靠轨道统一规定。
- 注意 Grid 版里仍然用了 Flex —— 这不是二选一,而是分层:外层 Grid 管列,内层 Flex 管卡片内部的纵向排列与底部对齐。
- 如果卡片内容长度差异很大(标题一两行、摘要长短不一),Grid 版加上
grid-auto-rows: 1fr可以强制每行等高,Flex 版则做不到。
动态演示
① 你需要同时控制行和列吗?
② 项目的尺寸由谁决定?
③ 需要「跨卡片对齐」吗?(每张卡的标题、按钮落在同一条线上)
Flexbox
短标题
内容宽度决定卡片宽度,剩余空间按 flex-grow 分配。
阅读一个明显更长的标题会占掉更多宽度
于是同一行里的按钮不在同一条线上。
阅读中等标题
卡片宽度随内容浮动。
阅读Grid
短标题
列宽由轨道统一规定,因此同一行的卡片永远等宽。
阅读一个明显更长的标题会占掉更多宽度
标题换行,但按钮依然对齐在同一条线上。
阅读中等标题
卡片等宽,节奏统一。
阅读改一下三个问题的答案,下方结论会实时更新,并在两个面板里高亮被选中的方案。注意第二个卡片:「按钮是否落在同一条线上」正是第三条判据的直观证据。