间距:写的 2px,看到的 9px
每段文字都带着一截看不见的空白,CSS 写的间距要加上它,才是眼睛看到的间距。AI 写的设置页里,2px 看上去是 9px,层次被压扁。讲清这截空白从哪来、为什么专坑小间距、中文页面怎么用 text-box 裁掉它,再给一段能贴进 DevTools 的 X 光脚本。
下面是 AI 写的一个设置页,可以在两种量法之间切换。「按 CSS 量」用的是浏览器排版时的盒子,margin、padding、gap 都从这里算起。「按眼睛量」只看屏幕上真正有颜色的像素。

| # | 位置 | 写的 | 看到的 | 多出 |
|---|---|---|---|---|
| 1 | 分组之间 | 24 | 27 | +3 |
| 2 | 分组标题 → 卡片 | 8 | 10.7 | +2.7 |
| 3 | 昵称 → ID | 2 | 11.5 | +9.5 |
| 4 | 标题 → 说明 | 2 | 9 | +7 |
| 5 | 说明 → 下一行 | 12.2 | 14.5 | +2.3 |
| 6 | 标签 → 输入框 | 8 | 11.2 | +3.2 |
| 7 | 输入框 → 提示 | 8 | 11.3 | +3.3 |
| 8 | 按钮 → 版本号 | 12 | 14.1 | +2.1 |
标题 → 说明是组内,说明 → 下一行是组间。写的:2 对 12.2,差 6.1 倍看到的:9 对 14.5,差 1.6 倍

这一份的 subagent 自己读了团队 UI 规范
| # | 位置 | 写的 | 看到的 | 多出 |
|---|---|---|---|---|
| 1 | 分组之间 | 24 | 27 | +3 |
| 2 | 分组标题 → 卡片 | 8 | 10.7 | +2.7 |
| 3 | 昵称 → ID | 4 | 13.5 | +9.5 |
| 4 | 标题 → 说明 | 4 | 11 | +7 |
| 5 | 说明 → 下一行 | 12.2 | 14.5 | +2.3 |
| 6 | 标签 → 输入框 | 8.8 | 12.1 | +3.3 |
| 7 | 输入框 → 提示 | 8 | 10.9 | +2.9 |
| 8 | 按钮 → 版本号 | 16 | 18.7 | +2.7 |
标题 → 说明是组内,说明 → 下一行是组间。写的:4 对 12.2,差 3.0 倍看到的:11 对 14.5,差 1.3 倍

| # | 位置 | 写的 | 看到的 | 多出 |
|---|---|---|---|---|
| 1 | 分组之间 | 24 | 27 | +3 |
| 2 | 分组标题 → 卡片 | 8 | 10.5 | +2.5 |
| 3 | 昵称 → ID | 4 | 12.5 | +8.5 |
| 4 | 标题 → 说明 | 2 | 8 | +6 |
| 5 | 说明 → 下一行 | 12 | 14.5 | +2.5 |
| 6 | 标签 → 输入框 | 8 | 10 | +2 |
| 7 | 输入框 → 提示 | 8 | 11 | +3 |
| 8 | 按钮 → 版本号 | 12 | 14 | +2 |
标题 → 说明是组内,说明 → 下一行是组间。写的:2 对 12,差 6.0 倍看到的:8 对 14.5,差 1.8 倍

第 1 份加上 text-box 裁剪,间距按看到的重排(作者改的)
| # | 位置 | 写的 | 看到的 | 多出 |
|---|---|---|---|---|
| 1 | 分组之间 | 24 | 24 | +0 |
| 2 | 分组标题 → 卡片 | 8 | 8.3 | +0.3 |
| 3 | 昵称 → ID | 4 | 6.5 | +2.5 |
| 4 | 标题 → 说明 | 4 | 4 | +0 |
| 5 | 说明 → 下一行 | 11.7 | 11.5 | −0.2 |
| 6 | 标签 → 输入框 | 8 | 8.1 | +0.1 |
| 7 | 输入框 → 提示 | 6 | 6.4 | +0.4 |
| 8 | 按钮 → 版本号 | 12 | 12.6 | +0.6 |
标题 → 说明是组内,说明 → 下一行是组间。写的:4 对 11.7,差 2.9 倍看到的:4 对 11.5,差 2.9 倍
蓝色虚线框是每段文字的行盒,CSS 的间距从它算起;红框是墨迹,也就是屏幕上真正有颜色的像素。色条标出八处间距,编号对应右边的表。 Chrome 156,375 px 宽的手机视口,macOS 系统字体。
同一处间距,两种量法差得很远。「邮件摘要」和它下面那行说明,CSS 里写的是 2px,屏幕上两行字之间隔了 9px。说明和下一行之间写的是 12px 左右,看到的是 14.5px。写的时候,组内和组间差 6.1 倍;看到的只差 1.6 倍。
这不是 AI 把数字写错了。三份页面的间距都取自一套固定的刻度,组内小、组间大,按 CSS 量,每一处都说得通。问题出在 CSS 的数字和眼睛之间:每段文字都带着一截看不见的空白,这截空白会加到它上下的每一个间距上。
这篇讲四件事:这截空白有多大、从哪来;为什么它专门把小间距撑大;中文页面里怎么把它去掉;怎么在任何页面上把它量出来。
AI 做对了什么
需求是一个移动端的「账户设置」页,prompt 在附录。三个全新的 Claude Code subagent 各写了一次,交出来的三份几乎一模一样:iOS 风格的分组列表,白卡片铺在浅灰底上,分组标题是灰色小字。
间距也写得规整:
- 三份都在
:root里先定义了一套间距变量:4、8、12、16、24、32px,前两份还有 2px 的半档。 - 关系也对。分组之间 24px,分组标题到自己的卡片 8px,标题和说明之间 2–4px,说明到下一行 12px。紧的地方紧,松的地方松。
- 第 2 份的 subagent 还自己读了团队的 UI 规范。规范按关系给间距分档,比如「4:同一个控件的内部零件」,例子里就有「标题与描述」。它照着在文件开头写了一张自己的对照表:4 给标题和描述,8 给分组标题和卡片、标签和输入框。
三个 subagent 交付前都在无头浏览器里截图检查过。它们汇报间距时,说的都是 CSS 里写的数字,比如第 3 份汇报的是分组之间 24px、分组标题到卡片 8px。按这些数字验收,三份都能过。要看出问题,得换一种量法。
每段文字都带着一截看不见的空白
邮件摘要
你的浏览器还不支持 text-box,「裁剪」两项用不了;Chrome 133、Safari 18.2、Firefox 154 起支持。
在你的浏览器里实时量的,用的是你系统里的字体;数字换算成 16 px 字号。墨迹按字体度量推算,在 macOS 的 Chrome 上和逐像素扫描比,每边差不到 0.5 px。蓝虚线是 CSS 给这行字的盒子,红框是字形真正画到的地方,色带是两者之间的空白,裁剪以后是冒出盒子的部分。
一行字在 CSS 里占的高度是它的行高(line-height)。16px 的字、行高 1.4,这一行占 22.4px。字形本身没有这么高:苹方的汉字从最高的一笔到最低的一笔,是字号的 0.94–0.98 倍,16px 的字只有 15px 出头。剩下的 7px 左右,分在上下两边。
这截空白有两个来源。一是行距:行高比字体本身高出来的部分,CSS 平分到上下两边。二是字体自己留的位置:字体的上伸、下伸本来就比大多数字形更高、更低,给重音符号和 g、y 的下伸部分留地方。CSS 的规范里写得很直接,这些额外的空白「interferes with visual alignment and with control over effective (visually-apparent) spacing」,会干扰视觉对齐,让人控制不了真正看得见的间距。
我们在 macOS 的 Chrome 上,用三份 AI 页面同款的字体栈(英文系统字体在前,苹方在后)量了一遍:12–24px、行高 1.5 时,汉字上下各空出 0.25–0.29 个字号。纯数字更矮,墨迹只有字号的 0.75–0.79 倍,上下各空出 0.32–0.40 个字号。
所以两块文字之间,看到的间距大约是:
看到的间距 ≈ 写的间距 + 上一块的下方空白 + 下一块的上方空白第 1 份里,「邮件摘要」是 16px、行高 1.4,量到下方空白 3.6px;说明是 13px、行高 1.4,上方空白 3.4px。两截加起来 7px,再加上写的 2px,正好是看到的 9px。昵称和 ID 也一样:昵称下方 4.5px,ID 是数字,上方空了 5px,加上写的 2px,看到 11.5px。
这也说明间距有个下限。我们把第 1 份里这些间距全改成 0 再量:「邮件摘要」和说明之间还有 7px,昵称和 ID 之间还有 9.5px,分组标题和卡片之间还有 2.5px 左右。不裁剪的话,在这套字号和 1.4 的行高下,两行字怎么写都贴不近。
为什么偏偏坑小间距
这截空白不看间距本来多大,加上去的都是差不多的几个像素。大间距多几个像素看不出来,小间距多几个像素就翻了好几倍。
标题 → 说明组内
说明 → 下一行组间
昵称 → ID组内
分组标题 → 卡片组内
分组之间组间
- 分组之间写 24,看到 27,只多了一成多。
- 分组标题到卡片写 8,看到 10.5–10.7,多了三成。
- 标题和说明写 2–4,看到 8–11,变成了原来的 2.75–4.5 倍。
层次靠的是「组内比组间近」。三份页面在 CSS 里都做到了:标题和说明是 2–4px,说明到下一行是 12px。可眼睛看到的是 8–11px 对 14.5px,组内和组间几乎一样远。
最明显的是昵称和 ID。三份里它在 CSS 上都写得很紧,只有 2–4px(第 1、2 份里,它和标题 → 说明并列最紧),按眼睛量却是 11.5–13.5px,比分组标题到卡片的 10.5–10.7px 还松。CSS 说昵称和 ID 比分组标题和卡片更亲近,眼睛看到的正好相反。
读过规范的第 2 份也一样。按它自己的对照表,4 给标题和描述,8 给分组标题和卡片。按看到的量,前者是 11px,后者是 10.7px,两档的顺序反了。分档管住了写的数字,管不住看到的。 分档的数字只要还是按行盒量,照着写就可能出现这种倒挂。
中文页面里,把这截空白裁掉
CSS 现在有专门做这件事的属性:text-box。它把文字盒子的上下边裁到指定的位置,裁完以后,写的间距就是看到的间距。2026 年 8 月起它已经是 Baseline:Chrome 133、Safari 18.2、Firefox 154 起都支持。
英文资料里的标准写法是这一行:
text-box: trim-both cap alphabetic;上边裁到大写字母的顶(cap),下边裁到基线(alphabetic)。对英文正合适,大写字母和数字都落在这两条线之间。
中文照抄会出问题。在上面的演示里切到「裁剪」:盒子小了,汉字的上下都冒了出去。原因是 text-box 按字体栈里第一个可用字体的度量来裁,而三份 AI 页面的字体栈都把英文系统字体放在最前面:-apple-system, BlinkMacSystemFont, "PingFang SC", …。裁剪线于是按英文字体的大写字母高度来定,比汉字矮了一截。
我们在 macOS 的 Chrome 上量了几种字体栈(13、16、20px):
| 字体栈里排第一的 | 裁剪后盒子的高度 | 汉字从上面冒出 | 汉字从下面冒出 |
|---|---|---|---|
BlinkMacSystemFont(英文系统字体) |
0.705 em | 0.15 em | 0.10–0.12 em |
system-ui |
0.705 em | 0.15 em | 0.10–0.12 em |
"Helvetica Neue" |
0.714 em | 0.15 em | 0.09–0.11 em |
"PingFang SC" |
0.86 em | 不冒 | 0.10–0.14 em |
-apple-system 在 Chrome 里不起作用:只写 -apple-system, "PingFang SC" 时,量到的结果和苹方排第一一模一样。
只裁不补,小间距会直接撞上。我们给第 1 份的所有元素都加上这一行再量:「邮件摘要」和说明之间写的是 2px,汉字一个往下冒、一个往上冒,墨迹叠进了对方 2px。
规范里其实给中文准备了专门的取值,ideographic-ink,裁到汉字墨迹的上下边。但 MDN 写着,这些取值「exact behavior is being debated and they are not supported by any browser」:具体行为还在讨论,目前没有浏览器支持。
所以现在能用的办法是,先按英文的线裁,再用 padding 把汉字冒出去的部分补回来:
@supports (text-box: trim-both cap alphabetic) { .label { text-box: trim-both cap alphabetic; padding-block: 0.16em 0.12em; }}在上面的演示里切到「裁剪后补回」,盒子就基本包住了汉字。三点要注意:
- 这两个值来自 macOS 上苹方加英文系统字体在前的实测:12–24px 时,汉字往上冒 0.11–0.17em,往下冒 0.09–0.13em。取 0.16em 和 0.12em,和实测最大值差不到 0.2px。Windows 的微软雅黑、Android 的思源黑体我们没有量过,换平台前先量一下。上面的演示和后面的脚本,都能在你自己的设备上量。
- 只有数字或英文的行没有往上冒的那一截,加了上面的 padding 会多出 2px 左右,可以只加下面的。
- 整段包在
@supports里。不支持的浏览器不裁剪,也不加 padding,间距保持原来偏大的样子,至少不会挤在一起。
修正版就是这么改的:第 1 份的单行文字加上这段 CSS,再把间距按「看到的」重新分配。改完以后,写的和看到的基本一致:分组之间写 24 看到 23.9–24.7,分组标题到卡片写 8 看到 7.6–8.4,标题和说明写 4 看到 4,标签到输入框写 8 看到 8.1。例外是昵称和 ID,写 4 看到 6.5:ID 是一串数字,没有往上冒的那一截,上面的 padding 补多了。层次也回来了:标题和说明 4px,说明到下一行 11.5px,差了将近 3 倍。开头那张图切到「修正后」,可以逐项对照。


拉到 5 px 左右再看:左边每样东西之间都隔着差不多的距离,右边的昵称和 ID、标题和说明、输入框和提示各自抱成一团。
把两张图一起模糊,看不清字,只剩一团一团的块,分组对不对一眼就能看出来。这就是设计师常说的「眯眼看」。
验收:给页面拍一张 X 光
开头那张图用的是逐像素截图:把每段文字设成透明,前后截图一比,变了的像素就是它的墨迹。这个办法最准,但要写脚本、开无头浏览器。日常验收用下面这段脚本就够了,贴进 DevTools 的 Console 回车就能用。
打开以后往下滚:蓝虚线是行盒,红框是墨迹,标签是这块文字到正下方最近一块东西的「写 · 看」间距,看到的比写的多 4 px 以上标红。只画当前屏幕上下各一屏。
它不截图,而是按字体的度量(canvas 的 measureText)推算墨迹的位置。我们拿它和逐像素截图在四份页面的 88 处间距上比过:误差中位数 0.4px,最大 0.9px。
可以先在这篇文章上试试。正文是 16.5px、行高 1.85,行高比前面的设置页大得多,空白也大得多:在我的电脑上(macOS 的 Chrome 156,1280px 宽),段落之间写的是 0.9em,约 15px,X 光量出来看到的是 29px,翻了一倍(输出)。
验收时看三件事:
- 组内和组间。 同一组里看到的间距,要明显小于组和组之间看到的间距。我自己的标准是不超过一半;这是经验值,不是哪份规范规定的。
- 最紧的那几对。 CSS 里写得最小的几处间距,按眼睛量是不是还比其他的小。昵称和 ID 就是在这一步露馅的。
- 眯眼看。 在 Console 里运行
document.body.style.filter = 'blur(5px)',看糊掉以后的团块是不是你想要的分组。
给 AI 的规则
AI 已经会用刻度、会分组内组间了,规则只需要补上它看不到的那一截:
- 文字相关的间距按「看到的」定:从字形的上下边量,不从行盒量。- 单行的标题、标签、说明,用 `text-box: trim-both cap alphabetic` 裁掉上下的空白, 再用 `padding-block: 0.16em 0.12em` 把汉字冒出去的部分补回来,整段包在 `@supports (text-box: trim-both cap alphabetic)` 里。字体栈不是「英文系统字体在前」时,先量再定这两个值。- 同一组里看到的间距,不超过组与组之间的一半。- 交付前在页面上跑一遍间距 X 光(xray-snippet.js),列出每组「写的 / 看到的」间距。附录:实测材料
三份页面由三个全新的 Claude Code subagent(Claude Opus 5.5)在 2026-10-29 各写一次,prompt 里只有需求。它们继承这台机器上的全局配置 ~/.claude/CLAUDE.md 和 skill 列表。据它们自己的汇报(摘录):第 2 份加载了团队的 UI 规范 skill(ui-conventions);第 1、3 份没有加载设计类 skill;第 2、3 份读过同目录下的 prompt.txt,内容和需求相同。
用 HTML + CSS + 少量原生 JS 写一个单文件页面:移动端 H5 的「账户设置」页。
内容从上到下:- 顶部导航栏:左边返回按钮,中间标题「账户设置」- 个人信息卡:头像、昵称「林小满」、ID「8812 3390」,右侧一个「编辑资料」按钮- 「通知」分组,三行: - 推送通知:右侧开关 - 邮件摘要:右侧开关;标题下面一行小字「每周一早上 9 点发送」 - 免打扰时段:右侧显示「22:00–08:00」和一个箭头- 「安全」分组,三行: - 登录密码:右侧显示「修改」和一个箭头 - 两步验证:右侧一个「未开启」标签 - 登录设备:右侧显示「3 台」和一个箭头- 「联系邮箱」表单:标签「邮箱」,输入框(占位文字「name@example.com」),输入框下面一行提示「用于接收账单和安全提醒」;输入的格式不对时,提示换成红色的错误信息- 每一行左侧有一个图标- 页面最底部:一个「退出登录」按钮,按钮下面一行小字版本号「版本 3.8.2」
把完整代码写到这个文件:……/settings1.html- 原样产出:settings1.html、settings2.html、settings3.html。作者改的:settings1-fixed.html(修正版)、settings1-zero.html(间距全改成 0 的对照)、settings1-trimall.html(所有元素只裁不补的对照)。三份都只在
<head>末尾单独加了一段样式,其余和原样一致。 - 逐像素测量:xray.mjs 把每段文字、每个图标逐个设成透明,和整页截图逐像素比,量出墨迹;relate.mjs 按文案找到这 13 处间距,算「写的」和「看到的」。原始输出在 data/ 下。
- 字形和裁剪:tools/ink-metrics.mjs 用 AI 页面同款字体栈,量汉字和数字上下的空白、裁剪后冒出多少(输出),tools/textbox-stack.mjs 量不同字体栈下裁剪后冒出多少(输出)。
- 浏览器版脚本:xray-snippet.js,和逐像素法的对比见 tools/snippet-validate.mjs(输出);在这篇文章上跑的是 tools/article-xray.mjs。
- 环境:macOS 上的 Chrome 156,375px 宽的手机视口,2 倍像素,系统字体。没有在 Safari、Windows、Android 和真机上量过。
text-box的浏览器支持情况来自 MDN 和 caniuse,ideographic-ink的说明来自 MDN,规范原文来自 CSS Inline Layout Module Level 3 的编辑草稿(2026-10-14)。