你以为页面孤字和段落寡妇只能靠设计师?今天 text-wrap 把这件事彻底原生化了

标题孤零零一个字、段落最后一行只剩一个词——设计师每次提我都要假装研究一下怎么修。这类问题存在了二十年,前端工程师的解法要么靠 JS 库、要么靠 <wbr> 硬断、要么干脆不管。今天 CSS 两行代码把它们彻底原生化了。

这个问题到底有多普遍

英文排版里有个专门的名词:「widow」,指的是段落最后一行只有一个单词的情况。「orphan」则指标题最后一行孤零零一个字。这两个词在排版行业里讨论了几十年,因为它们会让版面看起来像没做完——尤其是标题两行字数悬殊,第一行满满当当、第二行只有一个词,看起来像断了句。

在 CSS 出现之前,印刷工人靠手动调整字间距、行高、甚至在特定位置插入连字符来解决这个问题。浏览器时代,这个工作交给了 JS:流行库如 jQuery Widow Fix、Typist、Clamp.js,本质上都是在 DOM 渲染完之后再扫描一遍,找到孤字的位置,往回拉一个词,然后重新渲染。

代价你也知道:多一轮 layout 计算,响应式场景下每次 resize 都要重跑一遍,而且有时候会跟其他 JS 逻辑抢 DOM 导致闪动。

text-wrap: balance — 标题专用,字数均分

text-wrap: balance 是给标题设计的。浏览器不再采用「尽量塞满每行」的贪婪算法,而是把整个区块作为整体,用二分查找找到一个宽度,让所有行的字符数尽可能均衡。

h1, h2, h3, blockquote {
  text-wrap: balance;
}

效果大约是这样:

  • 默认:Crafting Meaningful Digital Experiences for Every User → 第一行很长,第二行只剩「User」
  • balance 后:Crafting Meaningful Digital Experiences / for Every User → 两行字数相近,视觉对称

Chrome 对 balance 的计算做了限制:超过 6 行的内容,浏览器会静默忽略这个属性(Firefox 放宽到 10 行)。这是有意设计的——balance 算法复杂度是 O(n²),对于短标题这点计算量可以忽略不计,但对于长段落,如果真的去全局优化所有断点,渲染性能会明显下降。

这也意味着:text-wrap: balance 只适合标题、引言、卡片标题这类短文本区块。放在长段落上,浏览器会直接忽略它,不会有任何副作用。

text-wrap: pretty — 正文专用,防止寡妇

text-wrap: pretty 是给正文段落设计的。跟 balance 不同,它不重新计算所有行的断点,而是只盯着最后 4 行:提前预判孤字/寡妇的情况,把倒数第二行的断点往前挪一点,让最后一行至少有两个单词。

p, li, dd {
  text-wrap: pretty;
}

pretty 还会优化以下情况:

  • 段落末尾恰好是连字符后的短词(pull back 避免行末孤零零一个 hyphenated word)
  • 行末标点(句号、逗号不应该在行首,但 greedy 算法有时候会把单字标点挤到下一行开头)
  • 整体 ragged-right 的节奏感(每行右边缘略微起伏,比僵硬对齐更易读)

pretty 的计算成本是固定的——窗口大小恒定,不随段落长度增长——所以它可以用在任意长度的正文中而不用担心性能问题。

两者可以同时用

一页网站里,标题和正文都存在孤字/寡妇问题。最简洁的写法:

:root {
  text-wrap: pretty;        /* 全局正文 */
}

h1, h2, h3, h4, blockquote, figcaption {
  text-wrap: balance;       /* 标题覆盖为更严格的均衡 */
}

两行声明,覆盖整站。这是 progressive enhancement 最佳实践:任何不支持这两个值的浏览器会静默回退到普通换行,没有任何副作用。

浏览器支持现状(2026 年 9 月)

属性值 Chrome Firefox Safari
balance 114+ 121+ 17.5+
pretty 117+ 134+ 17.5+

覆盖率:balance ~96%,pretty ~88%(Firefox 134 才加入)。对于正文内容,建议加一个 @supports 渐进增强:

p {
  text-wrap: pretty;
}

@supports (text-wrap: pretty) {
  /* 只有支持 pretty 的浏览器才应用,没支持的就用普通换行 */
}

三个坑

坑一:balance 在 flex/grid 里可能跟 intrinsic sizing 打架。 标题放在 display: flex 容器里,如果容器靠标题内容的自然宽度撑开,balance 的宽度优化可能会让容器重新 shrink,反而导致标题比预期窄。解法:给标题父容器加 flex: 1 1 auto 或设置明确 max-width

坑二:tabular numbers 不应该被 balance。 如果你的标题里有数字(日期、价格),balance 会让它们不再对齐。可以用 :has(> .tabular) 排除:

h1:not(:has(> .tabular)) {
  text-wrap: balance;
}

坑三:pretty 在 Firefox 134 以前不存在。 如果你的正文受众里 Firefox 占比不低,孤字问题在 Firefox 用户那里依然会出现。这是 graceful degradation 的场景:回退到普通换行,用户体验略微降级,但内容依然可读。

下一步

下次设计师提「标题第二行只有一个词看着别扭」,先别急着搜 JS 库,先问自己一个问题:是标题还是正文?

  • 标题 → 一行 text-wrap: balance
  • 正文 → 一行 text-wrap: pretty
  • 两个都有 → 两行全加上

如果项目里有现成的 Widowtamer、jQuery widow-fix 这类库,可以搜一下引用量,考虑逐步替换成这两行 CSS——体积减少几十 KB,而且不需要运行时计算。

排版的书设计师读了几十年,今天浏览器终于也在读了。

评论区

0 条评论

登录后可评论。