Pairwise
← 全部文章
UI间距排版AI审美

间距:写的 2px,看到的 9px

每段文字都带着一截看不见的空白,CSS 写的间距要加上它,才是眼睛看到的间距。AI 写的设置页里,2px 看上去是 9px,层次被压扁。讲清这截空白从哪来、为什么专坑小间距、中文页面怎么用 text-box 裁掉它,再给一段能贴进 DevTools 的 X 光脚本。

发布 阅读约 10 分钟

下面是 AI 写的一个设置页,可以在两种量法之间切换。「按 CSS 量」用的是浏览器排版时的盒子,margin、padding、gap 都从这里算起。「按眼睛量」只看屏幕上真正有颜色的像素。

第 1 份「账户设置」页的整页截图
第 1 份:八处间距,写的和看到的(px)
#位置写的看到的多出
1分组之间2427+3
2分组标题 → 卡片810.7+2.7
3昵称 → ID211.5+9.5
4标题 → 说明29+7
5说明 → 下一行12.214.5+2.3
6标签 → 输入框811.2+3.2
7输入框 → 提示811.3+3.3
8按钮 → 版本号1214.1+2.1

标题 → 说明是组内,说明 → 下一行是组间。写的:2 对 12.2,差 6.1 倍看到的:9 对 14.5,差 1.6 倍

蓝色虚线框是每段文字的行盒,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。按这些数字验收,三份都能过。要看出问题,得换一种量法。

每段文字都带着一截看不见的空白

邮件摘要

在你的浏览器里实时量的,用的是你系统里的字体;数字换算成 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 的行高下,两行字怎么写都贴不近。

为什么偏偏坑小间距

这截空白不看间距本来多大,加上去的都是差不多的几个像素。大间距多几个像素看不出来,小间距多几个像素就翻了好几倍。

标题 → 说明组内

第 1 份
第 2 份
第 3 份

说明 → 下一行组间

第 1 份
第 2 份
第 3 份

昵称 → ID组内

第 1 份
第 2 份
第 3 份

分组标题 → 卡片组内

第 1 份
第 2 份
第 3 份

分组之间组间

第 1 份
第 2 份
第 3 份
单位 px。图里这几处间距都多出差不多的一截:两行字之间多 6–9.5 px,文字和分隔线、卡片边之间多 2.3–3 px。大间距多这一截不显眼, 小间距被放大了好几倍。斜纹只画在第 1 份那一行:把第 1 份的这些间距全改成 0 以后,看到的还有这么多。
  • 分组之间写 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 倍。开头那张图切到「修正后」,可以逐项对照。

原样:AI 写的第 1 份,「账户设置」页整页截图
原样AI 写的第 1 份
修正后:加 text-box 裁剪,间距按看到的重排,「账户设置」页整页截图
修正后加 text-box 裁剪,间距按看到的重排

拉到 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,翻了一倍(输出)。

验收时看三件事:

  1. 组内和组间。 同一组里看到的间距,要明显小于组和组之间看到的间距。我自己的标准是不超过一半;这是经验值,不是哪份规范规定的。
  2. 最紧的那几对。 CSS 里写得最小的几处间距,按眼睛量是不是还比其他的小。昵称和 ID 就是在这一步露馅的。
  3. 眯眼看。 在 Console 里运行 document.body.style.filter = 'blur(5px)',看糊掉以后的团块是不是你想要的分组。

给 AI 的规则

AI 已经会用刻度、会分组内组间了,规则只需要补上它看不到的那一截:

写进 AGENTS.md 或 skill 的间距规则
- 文字相关的间距按「看到的」定:从字形的上下边量,不从行盒量。
- 单行的标题、标签、说明,用 `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,内容和需求相同。

prompt(三次只有最后一行的文件名不同)
用 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)。