Chapter 04
空间与布局
01 章说接近、对齐、留白靠间距表达,03 章说垂直节奏要落在统一基数上,却都没给出那套"基数"。这章先把间距收敛成一条阶,再讲清 Flexbox 和 Grid 到底怎么算尺寸——理解算法,布局就从"试边距"变成"选模型"。
本章你将建立的 schema
- 间距阶:所有 margin / padding / gap 从一条 8px 派生的有限数列里取
- Flexbox 是一维、content-out:grow / shrink / basis 是剩余空间的分配算法
- Grid 是二维、layout-in:先定轨道,内容再落格;fr / minmax / auto-fit 实现免断点响应式
- 现代响应式 = clamp() 流式 + 容器查询(组件响应自己的容器,不是视口)
布局是工程师最熟、也最容易"硬调"的领域:哪里不齐就加几个 px 的 margin,响应式就堆 media query。本章把这两件事换成有依据的做法——一套间距尺度,加上对两种布局引擎"怎么算"的理解。
4.1间距阶:别再随手填像素
间距来自一个基数(通常 8,有时 4)派生的有限数列(4 8 12 16 24 32 48 64);所有 margin / padding / gap 都从这条阶里取一档。
随手填间距(这里 7px、那里 13px、再来个 19px)会产生大量"差一点"的值:维护时无从判断该用哪个,视觉上也得不到一致的节奏。间距阶把无限的像素收敛成 ~8 档,每次"选一档",决策更快、结果天然一致——这正是全书那条"用有限尺度做选择"在空间上的落地。
底层机制(比文档深一层):为什么基数取 8?在常见的屏幕缩放比(1×、1.5×、2×)下,8 的倍数缩放后仍落在整数物理像素上,避免半像素带来的模糊;8 的因子多(2、4),便于二分出 4 这样的细档。数列本身不是纯等差——小间距端需要细粒度(组内微调 4 / 8 / 12),大间距端需要大跨度(区块分隔 32 / 48 / 64),所以越往大走,档与档的间隔越大(近似几何增长)。这条阶同时统一了 03 章的垂直节奏:排版间距和布局间距共用一套尺度,整页才有一致的"节拍"。
与下一节的关系:间距阶解决"留多少";"怎么排"则交给两种布局引擎,而它们的脾气完全不同——先看一维的 Flexbox。
4.2Flexbox:一维,从内容出发
Flexbox 沿一条主轴排列元素,先按内容尺寸摆好,再把剩余空间按规则分配;grow / shrink / basis 就是这套分配规则。
导航栏、工具条、按钮组、"标题在左按钮在右"——这些都是"沿一条线排,把多余空间分掉"的一维场景。理解 Flexbox 的分配算法,就能预测它的任何行为,而不是靠 justify-content 一个个试。
底层机制(比文档深一层):Flex 容器分三步算尺寸。① 每个子项有个起始尺寸 flex-basis(默认 auto = 内容自然宽);浏览器先按 basis 把所有子项排上主轴。② 算剩余空间 = 容器宽 − 所有 basis 之和:正值代表有余,负值代表溢出。③ 若有余,按各子项的 flex-grow 比例瓜分;若不足,按 flex-shrink × basis 的加权比例吸收。flex: 1 1 0(grow 1 / shrink 1 / basis 0)= 完全按 grow 比例等分,无视内容宽;flex: 0 1 auto(默认)= 不主动扩张、按内容宽、空间不足才收缩。把这条算法记住,"为什么这个项没撑开 / 为什么那个被压扁"立刻有答案。
flex-grow:1 给三项,是把剩余三等分加到各自 basis 上,所以最终宽度并不相等(basis 不同);③ 想要三项完全等宽,要 flex:1 1 0 把 basis 归零。与下一节的关系:Flexbox 管一条线。当布局是行列交织的二维网格时,需要另一套相反的思路。
4.3Grid:二维,先画格子
Grid 先定义行和列的轨道(track),内容再落进格子;fr 分配剩余、minmax + auto-fit 用一行代码实现免断点的响应式网格。
卡片网格、表格式布局、整页的"页头 / 侧栏 / 主区 / 页脚"——这些是行列同时要对齐的二维场景。用 Flexbox 硬凑二维要靠嵌套和换行 hack;Grid 直接描述网格,代码更短也更稳。
底层机制(比文档深一层):Grid 与 Flex 的思路正好相反——layout-in:你先用 grid-template-columns 画好轨道,内容自动落进格。三个关键原语:fr 是"剩余空间的一份"(1fr 1fr = 把剩余二等分);minmax(min, max) 让轨道在区间内伸缩;repeat(auto-fit, minmax(240px, 1fr)) = 自动塞入“尽量多、每列至少 240px”的列,容器变窄列数自动减少——这一行就是"卡片自动换行的响应式网格",一个 media query 都不用写。auto-fit 与 auto-fill 的差别:fit 会把没内容的空轨道折叠(剩余宽分给现有列),fill 保留空轨道占位。
/* 一行实现响应式卡片网格:尽量多列,每列≥240px,窄了自动减列 */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px; /* 取自间距阶;行列间距一把搞定 */
}
/* 没有任何 @media;列数随容器宽度自动变化 */
4.4选哪个、gap、与内在尺寸
判别(选 Flex 还是 Grid):问一个问题——这是一维还是二维?沿一条线排、长度由内容决定(导航、按钮组、tag 列表)→ Flex;行和列都要对齐成网格(卡片墙、整页框架、表单的标签列+输入列)→ Grid。两者可嵌套:Grid 搭整页骨架,每个格子内部用 Flex 排细节。
gap 优于 margin:元素之间的间距,优先用容器的 gap(Flex 和 Grid 都支持),而不是给子元素加 margin。原因:gap 只在相邻元素之间产生间距,没有"最后一个元素多出一段 margin"的问题,也不受 margin 折叠(margin collapse)的影响。这条直接消灭了 :last-child { margin: 0 } 这类补丁。
内在尺寸(intrinsic sizing):min-content / max-content / fit-content 让元素按"内容本身需要多宽"来定尺寸,而不是写死一个 width: 320px。用内在尺寸的布局更稳健——内容变长变短时不会溢出或留下大片空白。这与 Flex 的 content-out 是同一种思路:让内容说话,而不是强行规定盒子大小。
"标题在最左、一个按钮在最右、中间自动撑开"——该用 Flex 还是 Grid?用什么属性实现中间的撑开?
先想 10 秒再展开
一维,用 Flex。两种写法:① 给中间留白元素 flex:1 把它撑开;② 更常见——只放标题和按钮两个子项,容器 justify-content: space-between,剩余空间自动顶到中间。本质都是 §4.2 的"剩余空间分配"。
4.5响应式:从固定断点到流式 + 容器查询
现代响应式 = 用 clamp() 让尺寸随视口连续缩放 + 用容器查询让组件根据自己所在容器的宽度调整,而不是堆一堆视口断点。
传统 @media 断点有两个老问题:一是断点之间靠"跳变",中间尺寸不一定好看;二是它只看视口宽度,可同一个卡片组件放在宽主区和窄侧栏里,该有不同布局——视口宽度回答不了这个问题。
底层机制(比文档深一层),两件现代工具:
- clamp(min, preferred, max) 做流式。
font-size: clamp(1.5rem, 4vw, 2.5rem)让标题随视口宽度连续缩放,但不小于 1.5rem、不大于 2.5rem。一行代替一串断点,中间任何宽度都有合适值。间距同理可流式。 - 容器查询(@container)做真正可复用的组件。 给容器设
container-type: inline-size,组件内部用@container (min-width: 400px)查询父容器而非视口的宽度。于是同一个卡片组件,放进宽容器自动用横向布局、放进窄容器自动用纵向布局——它对自己的处境负责,不再依赖"我现在在哪个页面的哪个区域"。
/* 流式标题:随视口连续缩放,带上下限 */
.title { font-size: clamp(1.5rem, 1rem + 2.5vw, 2.5rem); }
/* 容器查询:卡片响应"自己的容器",不是视口 */
.card-wrap { container-type: inline-size; }
.card { display: grid; gap: 12px; }
@container (min-width: 400px) {
.card { grid-template-columns: 120px 1fr; } /* 宽了→图文横排 */
}
4.6收束:布局决策顺序
- 先定间距阶(base 8,向下细分 4),之后所有间距只从阶里取。
- 按维度选引擎:一维 Flex,二维 Grid;Grid 搭骨架、Flex 排骨架内的细节。
- 间距用 gap,不用子元素 margin,消灭 last-child 补丁。
- 尺寸优先内在(min/max/fit-content),少写死 width。
- 响应式先流式后断点:能
clamp()连续缩放就别堆断点;组件级适配用容器查询。
这套顺序把 01 章的接近 / 对齐 / 留白全部接到了可计算的尺度上:组内取小档、组间取大档是接近性;gap 对齐是对齐;留白就是阶里那几个大档。空间不再靠手感,而是从一条阶里选。
§本章 self-check
先合上教程,把答案写下来再展开对照。
- 间距阶为什么常以 8 为基数?为什么数列越往大端、相邻档间隔越大?
- Flexbox 分配空间的三步是什么?
flex:1 1 0和flex:0 1 auto的行为差别在哪? repeat(auto-fit, minmax(240px, 1fr))实现了什么效果?它替代了什么传统写法?- 容器查询解决了媒体查询的什么固有局限?举一个"同一组件在两处需要不同布局"的例子。
答案(先做完再展开)
- 8 在 1×/1.5×/2× 缩放下仍是整数物理像素(不糊),且因子多便于细分出 4。越往大端间隔越大,因为大间距(区块分隔)不需要细粒度,而小间距(组内)需要,所以数列近似几何增长。
- ① 按 flex-basis 排好;② 算剩余空间(容器宽−basis 之和);③ 有余按 grow 比例瓜分、不足按 shrink×basis 吸收。
1 1 0把 basis 归零→完全按 grow 等分(等宽);0 1 auto不主动扩张、按内容宽、不足才收缩(默认)。 - 自动塞入尽可能多的列、每列至少 240px,容器变窄时列数自动减少——免 media query 的响应式卡片网格。替代了"按断点手动改 grid 列数 / 用 float + 百分比宽 + 清浮动"的传统写法。
- 媒体查询只能看视口宽度,无法让组件根据它实际所在容器调整。例:一个用户卡片放在宽主区时图文横排、放进窄侧栏时图文竖排——只有容器查询能让同一份组件代码两处自适配。
用一套尺度 + 两种引擎,搭一个"设置页"骨架
布局:左侧固定 240px 的导航,右侧主区;主区内是若干"设置卡片",每张卡片是"标题 + 描述在左、一个开关在右"。要求:① 整页骨架用 Grid(写出 grid-template-columns);② 卡片内部用 Flex 实现"左描述右开关、中间自动撑开";③ 所有间距取自一条 8px 间距阶;④ 主区的卡片之间用 gap;⑤ 让导航在窄屏(或窄容器)下移到顶部变横向——说明你用媒体查询还是容器查询,以及为什么。
提示(卡住再展开)
① 外层 grid-template-columns: 240px 1fr。② 卡片 display:flex; justify-content:space-between; align-items:center,左侧标题+描述包一个 div(可 flex:1),右侧放开关。③ 卡片内边距 16 或 24、标题与描述间 4、卡片间 gap:16。④ 导航的"窄屏变横排":如果窄的判断依据是整个视口,用媒体查询即可;如果这个布局要做成可在任意容器里复用的组件,用容器查询更稳。两者都对,关键是讲清依据的是视口还是容器。