CSS 现在可以做出以前需要重置才能处理的字体选择。text-wrap: pretty 可防止孤字,容器查询单位可将标题缩放至其所在的列,而 text-box 可修剪行框中内置的半行距。其中一些功能尚处于实验阶段,浏览器支持也不均衡,因此需要防护机制而非盲目引入。
我整理了tidy-typography.css,将有用部分整合为默认样式表:渲染与回退规则、流体排版、垂直节奏、换行、连字符以及 OpenType 细节。
你可以直接使用整个文件,也可以仅提取解决眼前特定问题的声明。我将在本文中详细介绍所有细节,并在文末提供完整代码。
渲染与回退的合理性
html {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-synthesis: none;
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
body {
font-size-adjust: from-font;
}font-synthesis: none 可阻止浏览器在字体族缺少粗体或斜体时自行生成。平滑和文本渲染声明是渲染提示,主要针对 WebKit;结果仍因平台而异。
text-size-adjust 禁用移动端的字体膨胀,避免刻意设计的布局在旋转时发生跳动。font-size-adjust: from-font 利用字体指标减少回退字体替换网页字体时的视觉大小变化。
(与本文其他片段一样,此 :root 块仅为最终样式表的节选,最终样式表会将其与后面定义的额外自定义属性合并。)
流体排版
基于视口的流体排版无法知晓其元素有多宽。因此,窄侧边栏中的标题可能会像填满视口一样缩放。
.article-container {
container-type: inline-size;
}
:root {
--step-0: clamp(1rem, 0.9rem + 1cqi, 1.125rem);
--step-3: clamp(2rem, 1.6rem + 3cqi, 2.75rem);
}container-type: inline-size 使包装器成为容器查询,这样其子元素就可以使用 cqi:一种基于容器而非视口的单位。用 cqi 替换 clamp() 中的 vw,可使同一标题在文章列或 300px 侧边栏中适当缩放,而无需针对组件的媒体查询。
垂直节奏:修剪半行距
:heading, p, blockquote, figcaption, li {
text-box-trim: trim-both;
text-box-edge: cap alphabetic;
/* shorthand: text-box: trim-both cap alphabetic; */
}line-height 包含大写字母高度上方的空间和基线下方的空间:即半行距。当文本必须与其他元素紧密对齐时,这一点尤为明显。text-box-trim 可移除它,而 text-box-edge 则从大写字母的大写高度测量到字母基线。这是一种对字体特定的负边距微调的指标感知替代方案。
@supports selector(:heading) {
:heading {
text-box: trim-both cap alphabetic;
}
}Selectors Level 5 提议了 :heading,匹配 h1 到 h6 以及 [role="heading"]。它尚未在浏览器中落地,因此应将其置于 @supports selector() 检查之后。
平衡标题,无孤字的段落
:heading, blockquote, figcaption {
text-wrap: balance;
}
p, li, dd {
text-wrap: pretty;
max-width: 65ch;
}
[contenteditable="true"] {
text-wrap: stable;
}默认的标题换行可能会留下一行很短的最后一行。text-wrap: balance 将文本分布在其各行上,最适合最多约六行的文本块。对于段落,text-wrap: pretty 可避免孤立的最后一个单词,同时不对整个文本块进行平衡。两者都取代了 JavaScript 孤字修复程序。可编辑文本需要 text-wrap: stable,它可防止每个新单词重新排列段落。
正确调校的连字符
<html lang="en-GB">p {
hyphens: auto;
}hyphens: auto 依赖于浏览器的语言词典,因此文档需要正确的 lang 属性(例如 <html lang="en-GB">)。如果没有,浏览器就没有词典可用,无法对文本进行连字符处理。即使有正确的 lang,默认设置也可能留下极小的单词片段或连续多行连字符。
p {
hyphens: auto;
hyphenate-limit-chars: 6 3 3; /* word length, min before break, min after break */
hyphenate-character: "‐"; /* use proper hyphen glyph instead of dash */
hyphenate-limit-lines: 2; /* no more than 2 consecutive hyphenated lines */
hyphenate-limit-zone: 8%; /* reduce overall hyphenation frequency */
hyphenate-limit-last: always; /* never hyphenate the last word of a paragraph */
/* Safari legacy equivalents */
-webkit-hyphens: auto;
-webkit-hyphenate-limit-before: 4;
-webkit-hyphenate-limit-after: 3;
-webkit-hyphenate-limit-lines: 2;
}
pre, code, var, samp, kbd, :heading {
hyphens: manual;
-webkit-hyphens: manual;
}hyphenate-limit-chars 控制最小可断开的单词和片段;hyphenate-limit-lines 防止阶梯效应。hyphenate-limit-zone 使断行不那么频繁,而 hyphenate-limit-last 保护最后一个单词。Safari 需要带前缀的等效属性,因此需同时声明两组。
将代码、变量和标题设置为 hyphens: manual:断开的代码单词可能改变含义,而带连字符的标题通常读起来像是错误。
悬挂标点与墨迹感知的下划线
blockquote, p {
hanging-punctuation: first allow-end last;
}
a {
text-decoration-thickness: from-font;
text-underline-offset: 0.15em;
text-decoration-skip-ink: auto;
}标点符号的视觉重量不如字母,因此位于行首的引号看起来可能相对于其下方的文本略微缩进。hanging-punctuation 允许指定的标点符号稍微超出文本块的边缘。
first 值处理开头标点,last 值处理文本块末尾的结尾标点,而 allow-end 允许标点符号悬挂在右边缘之外,而不是强制换行。结果是文本块的边缘看起来真正齐平。
下划线也有类似的问题。浏览器应用扁平的、一刀切的厚度和位置,无论字体的实际粗细如何。
text-decoration-thickness: from-font 读取字体设计师意图的厚度。text-underline-offset 给降部留出呼吸空间,而 text-decoration-skip-ink: auto 会在降部穿过下划线的地方中断下划线,而不是直接穿过字母绘制。
可变字体的微调
body {
font-optical-sizing: auto;
font-variant-ligatures: common-ligatures contextual;
font-kerning: normal;
}
/* Dark mode grade compensation */
:root {
--vf-grad: 0;
}
@media (prefers-color-scheme: dark) {
:root { --vf-grad: -50; }
}
* {
font-variation-settings: "GRAD" var(--vf-grad, 0);
}可变字体通常包含光学尺寸轴,即 opsz。它使字体在大显示尺寸时使用较细的笔画,在小字号设置时使用较粗、更易读的笔画。
font-optical-sizing: auto 让字体根据其实际渲染大小进行该调整。@supports 块涵盖某些浏览器仍需要的旧版 font-variation-settings 语法,而 font-variant-ligatures 和 font-kerning 启用字体自身的连字和字距对。
深色背景上的浅色文字实际上比浅色背景上的相同粗细看起来更重。如果字体带有 GRAD(等级)轴,深色模式块会降低这种感知粗细,而不会像实际粗细变化那样改变字形形状或宽度。没有 GRAD 轴的字体可以安全地忽略该设置。
@font-face {
font-family: "Mono";
src: url("/fonts/mono-var.woff2") format("woff2");
size-adjust: 105%; /* tune per-font */
}将等宽字体混入正文通常会暴露相同声明 font-size 下的 x 高度不匹配问题。size-adjust 关键字在 @font-face 级别设置一次,专门缩放该字体的渲染输出,而不触动实际的 font-size 值。
数字、表情符号和脚本感知文本
表格中的数字需要相同的宽度,以便在屏幕上堆叠成整洁的列。相反,流动文本中的数字不需要同样的处理,应该自然地与周围的单词并排显示。
table, .tabular {
font-variant-numeric: tabular-nums lining-nums;
}
.prose {
font-variant-numeric: oldstyle-nums proportional-nums;
}
.emoji-color {
font-variant-emoji: emoji;
}
.emoji-mono {
font-variant-emoji: text;
}tabular-nums和lining-nums关键字为表格数字提供固定宽度和全大写字母高度。oldstyle-nums和proportional-nums让数字与小写字母并排显示,而不会突起破坏句子的纹理。
font-variant-emoji明确指定使用全彩还是单色表情符号。如果不设置,浏览器或平台将自行选择回退方案,结果可能比您预期的变化更大。在需要强制执行该选择的容器上应用.emoji-color或.emoji-mono,而不是在 body 上全局设置。
支持本地化的引号以及原生上下标
引号样式和字符顺序因语言和地区而异。英语将单引号嵌套在双引号内,英式英语则反转这一惯例,法语使用带有细空格的尖引号(guillemets)。
:where(:lang(en)) > * { quotes: "\"" "\"" "'" "'"; }
:where(:lang(en-GB)) > * { quotes: "'" "'" "\"" "\""; }
:where(:lang(fr)) > * { quotes: "«\\00202F" "\\00202F»" "\"" "\""; }
q::before { content: open-quote; }
q::after { content: close-quote; }quotes属性通过:lang()声明每种语言的顺序。原生的q元素随后通过open-quote和close-quote插入正确的配对。相同的标记可以在每个区域正确渲染,而无需在内容中硬编码字符。
将规则包裹在:where()中可使其特异性为零,因此项目可以使用一个简单的类来覆盖引号样式。
默认的sub和sup样式是合成的。浏览器只是缩小文本并移动基线,而不是使用专门为此位置设计的字形。
@supports (font-variant-position: sub) {
:is(sub, .sub) {
vertical-align: baseline;
font-size: 100%;
line-height: inherit;
font-variant-position: sub;
}
}
@supports (font-variant-position: super) {
:is(sup, .sup) {
vertical-align: baseline;
font-size: 100%;
line-height: inherit;
font-variant-position: super;
}
}font-variant-position切换到字体自带的专用下标和上标字形(如果存在),并重置vertical-align、font-size和line-height,以防止它们与实际字形的预期大小和位置冲突。大多数字体仅包含正确的数字而非字母,因此在广泛使用此功能前请检查特定字体。
关于特异性: :is() 和 :where()
:where()始终解析为零特异性,这对于重置很有用。此处每条规则都是基于观点的默认值,将其包裹在:where()中可以让项目用一个简单的类来覆盖属性。
:where(h1, h2, h3, h4, h5, h6) {
margin-block: 0.5em;
}:is()解决了另一个问题:重复。标题选择器列表在排版样式表中出现多次,对其进行分组可避免遗漏某个级别的风险。
:is(h1, h2, h3, h4, h5, h6), p, blockquote, figcaption, li {
text-box: trim-both cap alphabetic;
}与:where()不同,:is()携带其最具体参数的特异性。
最终的 tidy-typography.css
这是完整的重置代码,结合了稳定的层。不受支持的声明会被浏览器忽略,因此可以逐步采用该样式表。
/* ==========================================================================
tidy-typography.css — 现代排版重置
每条规则在不支持的浏览器中都能优雅降级。
========================================================================== */
html {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-synthesis: none;
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
body {
max-width: 800px;
margin: 0 auto;
padding: 1rem;
}
/* ---- 流体排版 ---- */
.article-container {
container-type: inline-size;
}
:root {
--step-0: clamp(1rem, 0.9rem + 1cqi, 1.125rem);
--step-1: clamp(1.25rem, 1.1rem + 1.5cqi, 1.5rem);
--step-2: clamp(1.6rem, 1.35rem + 2cqi, 2rem);
--step-3: clamp(2rem, 1.6rem + 3cqi, 2.75rem);
--step-4: clamp(2.5rem, 1.9rem + 4.5cqi, 4rem);
--vf-grad: 0;
}
@media (prefers-color-scheme: dark) {
:root {
--vf-grad: -50;
}
}
/* ---- 可变字体微调 ---- */
* {
font-variation-settings: "GRAD" var(--vf-grad, 0);
}
body {
font-size: var(--step-0);
font-size-adjust: from-font;
line-height: 1.6;
font-optical-sizing: auto;
font-variant-ligatures: common-ligatures contextual;
font-kerning: normal;
}
/* ---- 文本框修剪 ---- */
:is(h1, h2, h3, h4, h5, h6), p, blockquote, figcaption, li {
text-box: trim-both cap alphabetic;
}
/* ---- 文本换行 ---- */
:is(h1, h2, h3, h4, h5, h6), blockquote, figcaption {
text-wrap: balance;
font-size: var(--step-3);
line-height: 1.15;
letter-spacing: -0.01em;
}
h1 {
font-size: var(--step-4);
}
p, li, dd {
text-wrap: pretty;
max-width: 65ch;
hyphens: auto;
hyphenate-limit-chars: 6 3 3;
hyphenate-limit-lines: 2;
hyphenate-limit-zone: 8%;
hyphenate-limit-last: always;
}
pre, code, var, samp, kbd, :is(h1, h2, h3, h4, h5, h6) {
hyphens: manual;
}
blockquote, p {
hanging-punctuation: first allow-end last;
}
[contenteditable="true"] {
text-wrap: stable;
}
/* ---- 数字、表情符号和脚本感知文本 ---- */
table, .tabular {
font-variant-numeric: tabular-nums lining-nums;
}
.prose {
font-variant-numeric: oldstyle-nums proportional-nums;
}
.emoji-color {
font-variant-emoji: emoji;
}
.emoji-mono {
font-variant-emoji: text;
}
/* ---- 支持本地化的引号 ---- */
:where(:lang(en)) > * {
quotes: '"' '"' "'" "'";
}
:where(:lang(en-GB)) > * {
quotes: "'" "'" '"' '"';
}
q::before {
content: open-quote;
}
q::after {
content: close-quote;
}
/* ---- 原生下标和上标 ---- */
@supports (font-variant-position: sub) {
sub, .sub {
vertical-align: baseline;
font-size: 100%;
line-height: inherit;
font-variant-position: sub;
}
}
@supports (font-variant-position: super) {
sup, .sup {
vertical-align: baseline;
font-size: 100%;
line-height: inherit;
font-variant-position: super;
}
}
/* ---- 墨水感知下划线 ---- */
a {
text-decoration-thickness: from-font;
text-underline-offset: 0.15em;
text-decoration-skip-ink: auto;
}重置应包含的内容
本 CSS 中的许多部分消除了以前会泄漏到构建其他部分的冗余工作。text-wrap可以接管孤行处理任务,容器查询单位使组件的字型比例与其自身宽度保持关联,而text-box-trim避免了曾经用于补偿首字缩进的负边距。quotes、font-variant-position和font-optical-sizing将多个细节交还给语言和字体,而不是一堆一次性修复。
浏览器支持情况将继续变化,尤其是针对text-wrap和text-box-trim。不受支持的声明会被忽略,但这仅在未增强的排版本身可接受时才有效。
其中一些规则对于基础重置来说仍然太新。新闻文章、营销页面和可编辑的产品界面对其文本的要求各不相同;属于您的那套规则取决于字体、内容和您仍需支持的浏览器。