中文网页排版,多数主题都做错了
中文网页排版有个奇怪的现象:大家都觉得别扭,但很少有人能指出哪里别扭。
原因多半是这四个参数被直接沿用了西文的值。它们单独看都不致命, 叠在一起就是那种说不清的「不对劲」。
一、行高#
西文正文的常用行高是 1.4 到 1.5。这个值搬到中文上会显得挤。
因为汉字是方块字:笔画在字面内铺得很满,字与字之间没有西文那种 由升降部带来的天然空隙。同样行高下,中文的行间留白视觉上更少。
中文正文的行高应当在 1.6 到 1.8 之间。本站用的是 1.75。
还有个细节:行高要用无单位数值(line-height: 1.75),不要用 px 或 em。
无单位值是相对于元素自身字号计算的,继承时不会出乱子。
二、字重#
这条最容易被忽略,但效果立竿见影。
很多中文字体只有两个真实字重:400 和 700。 苹方有 Ultralight 到 Semibold,
思源黑体有 300 到 900 的完整字重;微软雅黑主要就是 400 和 700。
(Apple 官方系统字体清单里列有 PingFang SC Semibold,
见 System Fonts。)
这里的通行说法是「写 600 会触发伪合成,所以只能写 500」。这话只说对了一半。
伪合成的开关不是字重数值,而是 font-synthesis-weight。至于字重本身——
写一个字体没有的字重时,规范要求的是去找最接近的真实字重,不是伪造一个:
When a weight is specified for which no face exists, a face with a nearby weight is used. —— CSS Fonts Level 4 §2.2.2
「最接近」具体朝哪边找,规范给了确定规则(§5 字重匹配): 请求的字重大于 500 时,先按升序检查不小于目标的字重 (600 → 700 → 800 → 900),都找不到再向下找。于是:
- 苹方这类确有 600 的字体 → 请求 600 直接命中 600,不会跳到 700;
- 微软雅黑这类只有 400/700 的字体 → 请求 600 先查 700,落到 700。
两条路径都不会伪造粗体(前提是没开伪合成):开着合成时它才会把 400 算法加粗——笔画发虚、字形走样,而且每个浏览器合成得还不一样。
本站的做法是把合成直接关掉:
html {
font-synthesis-weight: none;
}
于是标题可以写 font-weight: 600:拉丁部分拿到 Archivo 的真实 600,
中文部分落到自己字体里最接近 600 的真实字重(苹方约 600、雅黑 700),
两者视觉重量相当。
(反过来,写 500 的话中文更容易掉到 400——比正文还轻,标题反而立不住。)
更重要的是:层级主要靠字号和颜色建立,不要靠字重硬拉。 一段 17px/400 的正文和一段 21px/600 的小标题,层级已经足够了。
勘误(2026-09-22):这一节原先写的是「正确做法:标题字重写 500, 让字体自己去挑最接近的真实字重」。结论和建议都改掉了:把伪合成归因到 字重数值本身是错的——开关是
font-synthesis-weight;而在关掉合成的实现里, 写 500 会让中文落到 400,比 600 更糟。 依据见 CSS Fonts Level 4 的字重匹配 与本站src/styles/base.css的font-synthesis-weight。
三、行宽#
这是四个里最麻烦的一个,因为中西文的最优值在同一个单位下对不齐。
西文的理想行宽是 45 到 75 个字符,66 是经典值。而 ch 这个单位
是为西文设计的——按规范,它等于渲染所用字体里「0」字形的宽度:
ch— Represents the typical advance measure of European alphanumeric characters, and measured as the used advance measure of the “0” (ZERO, U+0030) glyph in the font used to render it. —— CSS Values and Units Level 4
两个要点:它是欧洲字符的度量,而且取决于字体。所以 ch 换算不出一个
跨字体稳定的汉字数——换个字体,「66ch 能排几个汉字」这个数就变了。
粗估的话,一个汉字约等于两个 ch,66ch 大约排 33 个汉字。
(顺带一提,这个数其实落在中文舒适的 30–40 字里——但它是粗估,且随字体浮动。)
本站改用 em,图的是可预测:
.prose {
max-width: 34em;
}
em 等于元素的字号,这是规范保证的,且不依赖字体。
但「一个汉字正好 1em 宽」不是规范保证——汉字的 advance 取决于具体字体,
规范为此专门定义了一个 ic 单位来表示「字体中全角字形的典型 advance」,
并规定在无法确定时假设为 1em:
The
icunit is … a typically exact measure (in the few fonts with proportional fullwidth glyphs, an approximation) of a single fullwidth glyph’s advance measure. In the cases where it is impossible or impractical to determine the ideographic advance measure, it must be assumed to be1em. —— CSS Values and Units Level 4 §5.1.1
这一句本身就说明「容器宽度单位」和「实际字形宽度」不能混为一谈——
如果规范能保证两者相等,就不必再定义 ic 了。
所以准确的说法是:34em 让内容栏宽度稳定地跟随字号; 对常见全角汉字可近似理解为约 34 字,但实际每行字数仍受字体、标点、 字距和混排影响。于是:
- 中文:约 34 个字(近似,落在 30–40 的理想区间)✅
- 西文:约 68 个字符(近似,落在 45–75 的理想区间)✅
一个值同时覆盖两种文字的舒适区,而且容器宽度不随字体变。 这是
--measure: 34em 的由来。注意被保证的是「宽度稳定」,不是「字数确定」。
勘误(2026-09-22):这一节原先写的是「一个汉字大约等于两个
ch, 所以按 66ch 排中文,每行会排出约 130 个汉字——远超出中文舒适的 30 到 40 字」。 两处都错:按它自己的前提应当除以 2 得 33,而不是乘以 2 得 130; 而 33 本来就落在它自己说的舒适区间内,所以「远超」这个结论也不成立。这句话还遮住了一个更关键的事实:66ch ≈ 33em,与本题采用的 34em 宽度几乎相同 (这一步依赖「0 字形约 0.5em」这个假设,是推算而非实测)。 也就是说这个改动换来的是容器宽度的可预测性,不是行宽。 结论(用 em)不变,论证方式改了。
依据:CSS Values and Units Level 4 §ch、 §ic。
勘误(2026-09-24):上一条勘误之后,本节仍留着 「
em等于字号、与字体无关。一个汉字约 1em 宽……而且不随字体变」, 并据此把34em说成确定的字数换算。前半句对,后半句越界了: 规范保证的是em等于计算后的 font-size, 不保证每个汉字的 advance 恒等于 1em——所以「34em ≈ 34 字」是近似, 而「每行字数确定」这句话是不成立的。详见上面引的ic那段。
四、中西文间距与标点#
中文和英文、数字之间应该有一个小间隙:「共 500 条记录」比「共500条记录」好读。
过去这件事要靠 pangu.js 之类的库在
客户端跑一遍正则,找到边界然后插空格。它有一堆边缘情况(全角标点前后不加、
90° 不加、15% 不加),而且要在 DOM 加载后重写文本。
2026 年浏览器原生做了这件事:
html {
text-autospace: normal;
text-spacing-trim: trim-start;
}
text-autospace: normal自动在中西文之间插入约 1/4 个空格。2025 年进入 Baseline。text-spacing-trim: trim-start做中文标点的字距调整——行首的逗号句号 之类不再占满一格。Chrome / Edge 123+ 已默认开启。
两个前提:<html lang> 必须设置正确,否则浏览器不会应用对应语言的规则;
代码块里要关掉自动间距,因为代码里的空格是有意义的:
pre, code, kbd, samp { text-autospace: no-autospace; }
只有 Chromium 系支持:MDN 的浏览器兼容数据(2026-09-24 查)
记 Chrome 自 123 起支持,Safari 与 Firefox 均为「不支持」——
caniuse 上还没有这个特性(太新)。
所以它是渐进增强——不支持的浏览器只是少了那一点调整,版式不会坏。
本项目此前在这里写的是「全球覆盖约 72%」,现已删除。 那个数字没有任何来源(caniuse 抓不到、本项目也没实测过), 而它写在一个「实测」味道很重的段落里。 一个查不到来源的覆盖率,比不写更糟——读者会以为它是量出来的。
五、一条容易漏的:中文没有斜体#
中文字体不提供斜体字形。font-style: italic 对中文的结果是浏览器
合成倾斜——把笔画整体拉斜,字形变形、笔画糊在一起。
而 CSS 没法问「这段 <em> 里是中文还是西文」。所以务实的选择是:
统一把强调处理成加粗,这也是中文排版的通行做法。
em, i {
font-style: normal;
font-weight: 600;
}
六、字体栈的顺序#
最后一条,顺序不能反:
font-family: 'Archivo', -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
拉丁字体必须排在前面。 如果中文字体排在前面,英文也会用中文字体渲染—— 而中文字体里的拉丁字母是等宽的(为了和汉字对齐),排版上很难看。
顺序对了之后,英文用 Archivo 渲染,汉字自动落到苹方/雅黑,各取所长。
那要不要自托管中文字体#
默认不要。 中文字体单文件 5–20 MB,哪怕做了子集化,一个博客站也不该 为此付出首屏时间。而中文系统字体(苹方、微软雅黑、思源黑体)质量本身就很好—— 知乎、Medium 中文版这类大站的内文都用系统字体。
实在需要自定义时,用 cn-font-split
做子集化,并且务必用 unicode-range 限定覆盖面。
小结#
| 参数 | 西文常用 | 中文应用 |
|---|---|---|
| 行高 | 1.4–1.5 | 1.6–1.8 |
| 标题字重 | 600–700 | 600(配 font-synthesis-weight: none) |
| 行宽 | 66ch | 34em(≈34 汉字 / ≈68 西文字符,均为近似) |
| 中西文间距 | 手动加 | text-autospace |
| 强调 | 斜体 | 加粗 |
这些细节单独看都很小。但它们同时出现时,读者的感受就是「这个站读起来舒服」 而说不出为什么——那正是排版该有的样子。
参考#
- CSS Values and Units Level 4 §ch ——
ch的定义 (W3C Working Draft, 12 March 2024) - CSS Fonts Level 4 §2.2.2 Missing weights —— 缺少字重时如何匹配
- CSS Fonts Level 4 §font-synthesis-weight —— 伪合成的开关 (W3C Working Draft, 13 September 2026)
为什么链接带日期而不是
/TR/css-values-4/:后者是”最新版”, 会随规范修订移动——半年后回看,不知道当时引的是哪一版。 W3C 自己推荐的引用方式就是带日期的固定 URL。
修订记录
- 2026-09-12 发布。
- 2026-09-22 勘误:行宽一节把「一个汉字 ≈ 2
ch」算反了(应除以 2 得 33, 原写作乘以 2 得 130),连带「远超舒适区」的结论也不成立;字重一节把伪合成 归因到字重数值上,而开关其实是font-synthesis-weight,且在关掉合成的实现里 写 500 会让中文落到 400。两处的结论都保持不变(用em、标题用 600), 改的是论证与依据。同步修订了 中文排版。 - 2026-09-24 勘误:上一轮只改了那句算错的算式,却留下了更大的越界——
「
em等于字号、与字体无关」被推广成「一个汉字正好 1em 宽、每行字数确定」。 规范保证的只是em等于计算后的 font-size;它为此专门定义了ic来表示全角字形的典型 advance,无法确定时假设 1em。 同时苹方字重结论与 Apple 官方清单冲突(清单里确有PingFang SC Semibold), 且「请求 600 必落 700」是条件句:匹配算法对 >500 的请求先向上找, 有真 600 的字体直接命中 600。
被这些页面引用(1)
这一页也有 markdown 版本:/letterpress/cjk-web-typography.md
直接打开上面的 .md 链接即可读取;在支持内容协商的部署平台上, 也可以对页面 URL 发送 Accept: text/markdown。