第4章 CSS字体属性实战:从堆栈到OpenType特性

字体调优这件事,说简单也简单,说复杂也复杂。很多同学觉得「不就是设个 font-family 嘛」,结果上线后 Chrome 和 Safari 显示效果天差地别。嗯,今天我们就来把这几个核心属性彻底讲透。

本章核心脉络:font-family 堆栈设计 → font-weight 精确控制 → font-size 与行高配合 → font-variant 与 OpenType 特性。四步走,让你的字体在任何设备上都「稳如老狗」。

CSS 字体属性实战知识体系 字体显示调优 font-family 堆栈设计 font-weight 精确控制 font-size + line-height 配合技巧 font-variant OpenType特性 回退策略 字重映射 垂直韵律 连字/数字 OpenType 四个属性环环相扣,缺一不可。先定字体,再控粗细,然后调大小行高,最后用 OpenType 特性打磨细节。

4.1 font-family 堆栈设计:别让用户看到「默认宋体」

font-family 堆栈,说白了就是给浏览器准备一个「备胎列表」。你最喜欢的字体排第一,如果用户电脑上没有,就往后顺延。我见过太多项目只写了一个字体名,结果 Windows 用户直接看到宋体,整个设计感瞬间崩塌。

我的经验:堆栈至少写 3 层。第一层用你想要的字体,第二层用同类系统字体,第三层用通用字体族(serif/sans-serif)兜底。

来看一个实际案例。假设我们要用 PingFang SC 做中文字体,但 macOS 和 iOS 才有,Windows 上没有。这时候堆栈应该这样写:

/* 推荐的中文字体堆栈 */
body {
  font-family: 
    "PingFang SC",           /* macOS/iOS 首选 */
    "Microsoft YaHei",       /* Windows 首选 */
    "Hiragino Sans GB",      /* 旧版 macOS 备选 */
    "Noto Sans CJK SC",      /* Linux 备选 */
    sans-serif;              /* 终极兜底 */
}

/* 英文字体堆栈(搭配使用) */
body {
  font-family: 
    "Inter",                 /* 现代无衬线 */
    "Segoe UI",              /* Windows 系统 */
    "Helvetica Neue",        /* macOS 系统 */
    Arial,                   /* 通用备选 */
    sans-serif;
}

你想想看,为什么要把 sans-serif 放在最后?因为它是浏览器能识别的「通用字体族」,永远不会失效。我建议中英文混排时,把英文字体堆栈放在前面,中文字体堆栈放在后面,用逗号分隔。

注意:不要写太多字体名,3-5 个就够了。写太多反而增加浏览器匹配开销,而且用户电脑上不可能装那么多字体。

4.2 font-weight 精确控制:别被「bold」骗了

很多同学以为 font-weight: bold 就是 700,其实没那么简单。不同字体对字重的映射规则完全不同。我记得有一次项目里用了 Google Fonts 的 Noto Sans,设了 font-weight: 600,结果它直接降级到 400,因为 Noto Sans 只有 400 和 700 两个字重。

来看这张字重映射表,我建议你收藏:

数值 关键字 常见字体表现 我的建议
100 thin 极细,仅部分字体支持 慎用,屏幕显示容易看不清
300 light 细体,适合大标题 搭配大字号使用效果不错
400 normal 常规体,最稳定 正文首选,兼容性最好
500 medium 中等,介于 normal 和 bold 之间 适合强调性正文
600 semibold 半粗,很多字体不支持 先确认字体是否包含该字重
700 bold 粗体,几乎所有字体都有 标题和强调用,别滥用
900 black 特粗,仅部分字体支持 海报级标题可用

为什么会这样?因为 font-weight 的数值只是「建议值」,最终显示效果取决于字体文件本身是否包含该字重。如果字体只有 400 和 700,你设 500 或 600,浏览器会自动做「伪粗体」——说白了就是算法加粗,效果很丑。

避坑指南:我曾经在一个电商项目中用了 font-weight: 600,结果在 Firefox 上所有按钮文字都糊成一团。排查了半天才发现是字体没有 semibold 字重,浏览器做了伪粗体。从那以后,我每次用非标准字重都会先检查字体文件。

正确的做法是:用 @font-face 加载字体时,明确指定每个字重的 src:

@font-face {
  font-family: 'MyFont';
  src: url('myfont-regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
}

@font-face {
  font-family: 'MyFont';
  src: url('myfont-bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
}

@font-face {
  font-family: 'MyFont';
  src: url('myfont-semibold.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
}

/* 使用时直接写数值,浏览器会匹配正确的字体文件 */
h1 { font-family: 'MyFont', sans-serif; font-weight: 700; }
h2 { font-family: 'MyFont', sans-serif; font-weight: 600; }
p  { font-family: 'MyFont', sans-serif; font-weight: 400; }

4.3 font-size 与 line-height 配合:垂直韵律的秘密

字体大小和行高,这两个属性是「连体婴」。单独调 font-size 不考虑 line-height,排版就会忽松忽紧。我个人习惯用「无单位行高」,也就是 line-height: 1.6 这种写法,它相对于当前 font-size 计算,省心很多。

来看一个实际对比:

场景 font-size line-height 说明
正文 16px 1.6(约 25.6px) 阅读舒适,行距适中
大标题 32px 1.2(约 38.4px) 紧凑,节省垂直空间
小标题 20px 1.4(约 28px) 介于正文和标题之间
引用/注释 14px 1.8(约 25.2px) 行距稍大,便于阅读

你想想看,为什么正文行高要 1.6 而标题只要 1.2?因为正文需要连续阅读,行距太密眼睛容易串行;标题是扫读的,行距太宽反而显得松散。嗯,这里要注意:行高不要用 px 写死,否则用户缩放字体时行高不会跟着变。

我的小技巧:用 CSS 自定义属性统一管理行高,方便全局调优:

:root {
  --line-height-tight: 1.2;
  --line-height-normal: 1.6;
  --line-height-loose: 1.8;
}

body {
  font-size: 16px;
  line-height: var(--line-height-normal);
}

h1 {
  font-size: 2rem;
  line-height: var(--line-height-tight);
}

还有一个容易被忽略的点:中英文混排时,行高要适当调大。因为中文的方块字和英文的 x-height 差异很大,同样的 line-height,中文看起来比英文挤。我一般会在中英文混排的段落里把 line-height 加到 1.7 或 1.8。

4.4 font-variant 与 OpenType 特性:专业排版的「隐藏技能」

font-variant 这个属性,说实话很多前端同学都没用过。但它能调用的 OpenType 特性,是专业排版和普通排版的分水岭。OpenType 字体里内置了很多「特性开关」,比如连字、数字样式、分数、花体等等。

来看几个最常用的场景:

/* 1. 启用标准连字(fi, fl 等) */
body {
  font-variant-ligatures: common-ligatures;
}

/* 2. 使用等宽数字(表格对齐神器) */
.price {
  font-variant-numeric: tabular-nums;
}

/* 3. 启用旧式数字(有上下延伸,更古典) */
.old-style {
  font-variant-numeric: oldstyle-nums;
}

/* 4. 分数显示 */
.recipe {
  font-variant-numeric: diagonal-fractions;
}

/* 5. 小型大写字母(标题用) */
.caps {
  font-variant-caps: small-caps;
}

为什么会用到等宽数字?我举个例子。你在做价格列表时,如果数字不是等宽的,「1」和「8」占的宽度不一样,整列数字就会左右晃动,非常难看。用 tabular-nums 后,所有数字宽度一致,表格瞬间整齐了。

避坑指南:我曾经在一个金融项目中,所有金额数字都没开等宽特性。结果 QA 测试时发现,¥1,234 和 ¥9,876 在表格里对不齐,小数点后的数字歪歪扭扭。后来加了一行 font-variant-numeric: tabular-nums,问题秒解。

OpenType 特性还有一个「杀手级应用」:连字。比如英文里的 fi、fl、ffi 等组合,默认显示时字母 i 的点会和 f 的勾重叠,非常丑。启用连字后,字体会自动用连字字形替换,看起来就像手写一样流畅。

/* 完整启用 OpenType 特性 */
.text {
  font-variant: common-ligatures
                tabular-nums
                lining-nums;
}

/* 或者用简写属性(兼容性稍差) */
.text {
  font-feature-settings: "liga" 1, "tnum" 1, "lnum" 1;
}

注意:font-feature-settings 是底层属性,兼容性更好,但写法比较啰嗦。font-variant 是高级封装,更语义化。我建议优先用 font-variant,遇到兼容性问题再降级到 font-feature-settings。

最后分享一个我常用的「字体调优检查清单」:

  • font-family:堆栈是否覆盖了主流系统?有没有写通用字体族兜底?
  • font-weight:使用的字重是否在字体文件中真实存在?有没有伪粗体风险?
  • font-size + line-height:行高是否用无单位值?中英文混排是否调大了行高?
  • font-variant:数字是否需要等宽?连字是否启用?有没有用到 OpenType 特性?

这四个属性调好了,你的字体显示效果至少能提升 80%。剩下的 20% 是细节打磨,比如字间距、段落间距、首行缩进等,我们后面的章节会继续聊。