2026-10-03 更新:本文改用 CSS 规范原文(CSS 2.2、CSS Inline Layout 3、Flexbox、Box Alignment/Gaps)与设计体系官方文档重写,删去原先的本机测量数值,改为给出规范级原因与可复制的修正写法。
一个工具栏里几个元素间距都设成 16px,截图一看还是有的挤、有的松;标题上下内边距写成一样,字看起来总是偏上。这类"数值一样、观感不一样"的问题,根子不在审美,而在几个规范层面的机制:半行距、外边距合并、基线对齐,以及 gap 只保证盒边缘等距。
一、规范级根因:半行距(half-leading)
CSS 2.2 的 §10.8.1 Leading and half-leading 把行距算得很具体:设字体的 ascent 为 A、descent 为 D,行盒高度由 line-height 决定,中间差出来的部分
L = line-height − (A + D)称为 leading,规范要求一半加在字形上方、一半加在下方:
A′ = A + L/2 D′ = D + L/2规范的原文表述是:行内盒的高度囊括所有字形及其两侧的半行距,因此恰好等于 line-height。
问题就出在这里:A 和 D 由字体决定,两者通常不相等——上方留白按 A 分配、下方按 D 分配,字号越大、行高越大,这种不对称越明显。("看起来偏上/偏下"是由该算法推出的结论,规范本身只定义算法,不评价观感。)
设计体系层面同样承认这个机制:Material Design 3 的排版文档在讲 Web 端实现时,直接说明文字在边界盒内垂直居中,遵循 CSS 建立的 half-leading 行为。规范也给了未来的解法:CSS Inline Layout Level 3 定义的 text-box-trim(修剪首/末行盒的半行距)与 text-box-edge(指定用哪套文字度量来裁),目的就是让内容盒边缘贴合文字。注意它目前是 Editor's Draft(工作草案,可能变更),MDN 记载它在 2026 年 8 月起于最新浏览器可用——生产环境要用,得配 @supports 或先接受降级。
可以复制的对照写法:
/* 半行距导致上下留白不对称:盒子上下到墨迹的距离由字体的 A / D 分开决定 */
.optical-tight {
font-size: 16px;
line-height: 1.5; /* 行盒 24px;L = 24 − (A+D),L/2 各加在上、下 */
}
/* 补偿写法:先把行盒压到接近字形高度,再用块轴内边距配平上下 */
.optical-balance {
line-height: 1;
padding-block: .35em .5em; /* 不对称补偿,让墨迹上下净空相等 */
}
/* 规范级的正解(草案,需特性检测) */
@supports (text-box: trim-both cap alphabetic) {
.optical-balance { line-height: 1.5; padding-block: 0; text-box: trim-both cap alphabetic; }
}二、第二个来源:外边距会合并
CSS 2.2 §8.3.1 Collapsing margins 规定:两个或多个盒的相邻上下外边距会合并成一个外边距;多个外边距合并时结果为其中的最大值(负值另有规则)。
于是 "16 + 16 = 32" 这种预期经常落空:相邻块级元素之间实际是 16。gap 之所以被大量使用,一个原因就是它不参与合并,语义上就是"两者之间的距离"——要精确控制间距,用 gap 比叠 margin 可预期得多。
三、第三个来源:基线对齐留下的"看不见的空隙"
- CSS 2.2 §10.8.1 规定:
inline-block的基线是它最后一行行盒的基线;如果它没有流内行盒,或者overflow的计算值不是visible,则基线取下外边距边缘。 vertical-align: baseline的语义是"把盒的基线与父盒的基线对齐"。替换元素(如<img>)默认按基线参与行内布局,而基线之下还有一段 descent 的空间——这就是图片底部那条缝的来源。
修法是二选一:给图片 display: block,或用 vertical-align: middle / bottom 显式对齐(MDN 对 vertical-align 的定义是设置 inline、inline-block 或 table-cell 盒的垂直对齐)。
/* 图片底部空隙的两种修法 */
.article img { display: block; } /* 脱离行内布局 */
.toolbar img { vertical-align: middle; } /* 仍参与行内布局时显式对齐 */Flex 布局下是另一套语义:Flexbox 规范 §8.3 说明,当 align-items / align-self 取 baseline 时,同一行所有参与基线对齐的项按基线对齐,基线到交叉轴起始外边距最大的项贴住该行起始边;若某项在所需轴上没有基线,就合成一个基线(所以"看起来没对齐"往往是因为某个子项被合成了基线)。
四、第四个来源:gap 只保证"盒边缘"等距
CSS Gaps Module Level 1 §2 定义了 gap / row-gap / column-gap:在 multi-column、flex、grid 中为相邻盒提供间距;用于 flex 容器时,主轴表示相邻 flex 项之间的固定间距,交叉轴表示相邻 flex line 的间距。(CSS Box Alignment Level 3 已把该章节移交给该模块,MDN 的 gap 页面表述与此一致。)
关键在于:gap 量的是盒边缘,不是墨迹边缘。视觉净空 = gap + 两侧各自的内部余量。一个有边框、有内边距的按钮,和一个只有图标(墨迹内缩)的按钮并排时,边缘等距不等于观感等距——这就是"设成一样还是不一样"的最终答案。
按项补偿的写法(可复制):
.toolbar {
display: flex;
gap: 16px;
align-items: center;
}
/* 墨迹比盒小的项,按侧补偿掉它自己那部分内缩 */
.toolbar > .icon-only { margin-inline: -3px; } /* 无边框图标:两侧都多 */
.toolbar > .text-btn { margin-inline: 0 6px; } /* 文字右侧自带字距余量 */用逻辑属性而不是 margin-left/right,在 RTL 语言下不用重写(MDN 对 padding-block / margin-block 的说明:它们按书写模式映射到物理属性)。
五、把"视觉净空"写成一个可调的数值
与其逐处试数,不如把它显式参数化:把行盒压到字形高度,用不对称内边距承载"看上去该有的留白",让净空变成一个变量。
:root {
--optical-gap: .35em; /* 视觉净空:唯一需要调的旋钮 */
}
.stack > * + * {
margin-block-start: calc(var(--optical-gap) * 2); /* 不依赖 gap 的兜底写法 */
}
.panel-title {
line-height: 1;
padding-block: var(--optical-gap) calc(var(--optical-gap) * 1.4); /* 上小下大,抵半行距 */
}调参时记住三条:字号变了要重算(A/D 随字号变),字体换了也要重算(不同字体的 ascent/descent 不同),行高变了同样影响上下分配。
六、按内容类型分类处理
| 相邻关系 | 参考边界 | 建议做法 |
|---|---|---|
| 文本 ↔ 文本 | 行盒边缘 | 用 margin-block,注意半行距造成的上下不对称 |
| 有边框的元素 ↔ 有边框的元素 | 边框 | gap 即可,边缘即墨迹 |
| 无边框图标 ↔ 文字 | 墨迹 | gap + margin-inline 按项补偿 |
| 图片 ↔ 文本 | 图片墨迹 | 图片 display: block,再谈间距 |
| 跨状态(hover/focus 出现描边) | 描边后的边界 | 描边不要用会改变盒尺寸的写法(改用 outline / box-shadow) |
七、几条能落地的规则
- 同一排元素,先统一参考边界(有边框看边框、无边框看墨迹),再谈数值;
- 能不用外边距合并的地方就避免依赖它——需要精确间距时用
gap; - 图标类元素与文字类元素混排时,按项补偿而不是全局调 gap;
- 半行距造成的不对称,用"行高压到 1 + 不对称
padding-block"抵消,别用等值内边距硬凑; - 描边类状态变化不要改变盒模型尺寸,否则一组数字全部失效。
参考来源
- W3C, CSS 2.2 §10.8.1 Leading and half-leading(A、D、L 与行盒高度)— https://www.w3.org/TR/CSS22/visudet.html
- W3C, CSS 2.2 §8.3.1 Collapsing margins — https://www.w3.org/TR/CSS22/box.html
- CSSWG, CSS Inline Layout Module Level 3(
text-box-trim/text-box-edge,工作草案)— https://drafts.csswg.org/css-inline-3/ - CSSWG, CSS Flexible Box Layout Module Level 1 §8.3 align-items(基线对齐与合成基线)— https://drafts.csswg.org/css-flexbox-1/
- CSSWG, CSS Gaps Module Level 1 §2 Gaps Between Boxes — https://drafts.csswg.org/css-gaps-1/
- CSSWG, CSS Box Alignment Module Level 3(gap 章节去向)— https://drafts.csswg.org/css-align-3/
- MDN, line-height — https://developer.mozilla.org/en-US/docs/Web/CSS/line-height
- MDN, vertical-align — https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align
- MDN, gap — https://developer.mozilla.org/en-US/docs/Web/CSS/gap
- MDN, padding-block — https://developer.mozilla.org/en-US/docs/Web/CSS/padding-block
- MDN, margin-block — https://developer.mozilla.org/en-US/docs/Web/CSS/margin-block
- MDN, text-box-trim — https://developer.mozilla.org/en-US/docs/Web/CSS/text-box-trim
- Material Design 3, Spacing(8dp 基准、padding / gap / margin 三类)— https://m3.material.io/styles/spacing
- Apple, Human Interface Guidelines — Layout — https://developer.apple.com/design/human-interface-guidelines/layout