网页字体当然要看起来合适,但只挑一个字体名称还不够。浏览器最终怎样显示文字,取决于字体家族中有哪些字形文件、它怎样匹配这些文件,以及下载字体时网络是否顺畅。我们实际要处理的是几个很具体的问题:首选字体不可用时用什么替代?粗体和斜体从哪个文件取得?字体还没下载完时,文字能否先显示?高级字形和可变轴又该交给哪一层 CSS?
下面就沿着浏览器实际处理字体的过程往下看。我们先安排字体栈和自定义字体,再处理字重、尺寸、OpenType 特性与可变字体。最后还会回到字体匹配过程,看看 font 简写为什么常常让前面的设置失效。
页面内容不该依赖某个字体文件才能阅读。网页字体可能延迟或加载失败,也可能被用户样式、辅助扩展替换。因此,页面需要允许文字先用回退字体显示,并且在字体没有成功加载时仍然可读。
我们平时说的“一款字体”,通常指一个字体家族。一个家族里可以有常规、粗体、斜体、粗斜体或窄体等多个字形面。传统字体往往把这些字形面分别放在不同文件中;可变字体则能在一个文件里提供连续变化的字重、宽度或倾斜轴。
CSS 提供五类基础通用族:
除此之外,现代规范还定义了 system-ui、ui-serif、ui-sans-serif、ui-monospace、emoji、math、fangsong 等更具体的通用族。它们在不同浏览器和系统上的实际字体可能不同,使用前要在目标平台上确认效果。
font-family 接受按优先级排列的逗号列表。浏览器从左向右尝试,最后应放一个通用族兜底:
body {
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
}
code,
pre,
kbd {
font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
}具体字体的家族名如果包含空格、标点,或者恰好与关键字同名,需要加引号。通用族关键字则不能加引号:"serif" 是在寻找一个名字恰好为 serif 的字体,serif 才是让浏览器选择系统中的衬线字体。单双引号都可以,只要前后配对;如果 CSS 写在 HTML 的双引号 style 属性里,内部一般改用单引号。

你可以修改首选字体名,也可以切换列表末尾的通用族。下方实验会立即显示计算后的 font-family。留意其中的代码元素:浏览器默认样式有时会给它单独指定字体,从而覆盖父元素的继承结果。
@font-face 做的事情是注册字体资源,而不是把字体直接应用到页面。注册完成后,元素仍要通过 font-family、font-weight、font-style 等属性提出请求,浏览器才会为它匹配相应的字形面。
@font-face {
font-family: "Site Sans";
src:
local("Site Sans Regular"),
url("/fonts/site-sans-regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
html {
font-family: "Site Sans", system-ui
这条规则里,font-family 和 src 是必需描述符。前者给字形面指定家族名,后者列出资源来源。src 中的候选项有先后顺序:可以先用 local() 查找本机字体,再用 url() 请求网络文件,浏览器会采用第一个可用来源。format() 用来提示文件格式,tech() 用来说明增量字体、颜色字体或可变特性等技术能力。浏览器会跳过无法识别的来源,因此通常把 WOFF2 放在靠前的位置。
描述符和同名属性分工不同。@font-face 中的 font-weight: 700 是在说明“这个文件提供 700 字重”,不会把文件中的字形强行变粗。写在元素上的 font-weight: 700,才是在请求浏览器匹配 700 字重。
非可变字体通常需要注册多个文件。此时每条规则都使用同一个 font-family 名称,再分别写清文件对应的字重和样式:
@font-face {
font-family: "Site Sans";
src: url("/fonts/site-sans-regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: "Site Sans";
src: url("/fonts/site-sans-bold-italic.woff2") format("woff2");
这样一来,组件只需声明所需的粗细和样式,不必知道具体文件名。部署字体时还要确认使用授权、同源策略或 CORS 响应、MIME 类型是否正确。文件较大时,可以配合缓存和字体子集减少传输量。
unicode-range 可以限定一个字形面负责哪些 Unicode 字符。浏览器会检查页面实际出现的字符,再决定是否下载对应资源,所以我们可以把拉丁字符、中文和符号拆成不同子集:
@font-face {
font-family: "Reading Sans";
src: url("/fonts/reading-sans-latin.woff2") format("woff2");
unicode-range: U+0000-00FF;
}
@font-face {
font-family: "Reading Sans";
src: url("/fonts/reading-sans-cjk.woff2") format("woff2");
unicode-range: U+4E00-9FFF;
范围可以写成单个码位、起止区间,也可以使用 U+4?? 这样的通配形式。不同规则的范围可以重叠,只是匹配过程和后续维护会更难判断。还要确认字体文件确实包含声明的字符;仅仅写了范围,并不能补上文件里不存在的字形,缺字时浏览器仍会逐字符回退。
字体资源还没准备好时,font-display 决定浏览器先怎样处理文字:

从回退字体切换到网页字体时,字符宽度和行高可能变化,页面也会随之偏移。size-adjust 可以按百分比调整某个 @font-face 的轮廓与度量,ascent-override、descent-override、line-gap-override 则分别覆盖上升部、下降部和行间隙。实际使用时,我们通常用它们校准本地回退字体,让切换前后的排版更接近:
@font-face {
font-family: "Reading Fallback";
src: local("Arial");
size-adjust: 102%;
ascent-override: 91%;
descent-override: 23%;
line-gap-override: 0%;
}元素属性 font-size-adjust 关注的是 x 高、帽高、ch 宽度,或 CJK “水”字的宽高等指标。换用回退字体时,它能让关键字形保持接近的可读尺寸。这个属性和描述符 size-adjust 名字相近,但作用位置不同:font-size-adjust 调整元素的字体尺寸,size-adjust 校准一个已经注册的字形面。
font-weight 可以使用 1 到 1000 的数值,也可以写 normal、bold、bolder 或 lighter。其中 normal 等同于 400,bold 等同于 700。这些数值表示要匹配的字重坐标,并不代表字体一定提供了对应粗细。如果非可变字体只有几个现成字形面,多个数值可能都会匹配到同一个相近字形。
.body-copy { font-weight: 400; }
.lead { font-weight: 520; }
.title { font-weight: 750; }bolder 和 lighter 依据父元素的计算字重选择更粗或更细的档位,不是简单地加减 100。只需要表达相对强调时,它们很方便;如果设计系统规定了明确的字重层级,直接使用数值更容易核对和维护。
如果字体提供连续字重轴,可以在 @font-face 中声明它覆盖的区间:
@font-face {
font-family: "Reading Variable";
src: url("/fonts/reading-variable.woff2") format("woff2");
font-weight: 200 900;
}字体没有对应粗体时,浏览器可能人工合成一个粗体效果。你可以用后文的 font-synthesis 禁止合成,不过更基础的检查是让声明范围与字体真实的轴范围一致。请求值越过范围时,浏览器只能取边界值,或者改用其他字形面。
font-size 支持长度、百分比,以及绝对和相对关键字。它们的参照方式各不相同:
xx-small 到 xxx-large,由浏览器按比例映射。larger 与 smaller 相对父元素的计算字号选择更大或更小一级。em 与百分比相对父元素字号;rem 相对根元素字号。px 提供稳定的 CSS 像素尺度,但仍会受到页面缩放与用户设置影响。clamp() 可把流式字号限制在可读区间。html { font-size: 100%; }
body { font-size: 1rem; }
h1 { font-size: clamp(2rem, 5vw, 4.5rem); }
.meta { font-size: 0.875em; }百分比是基于父元素的计算字号继续计算的。例如父级设为 125%,子级再设为 80%,最终大致会回到上一级的原始尺寸。在嵌套较深的组件里反复使用 em,字号很容易一层层累乘。全局字号层级用 rem 通常更好控制;组件内部需要跟随当前字号变化的间距,则可以继续使用 em。
不同字体即使都设为 16px,小写字母看起来也可能一大一小。font-size-adjust 就是用来处理这种差异的。它默认依据 x 高比率调整,也可以指定 cap-height、ch-width、ic-width 或 ic-height;from-font 尝试读取字体内置指标,none 则关闭调整。它不会让所有字体的外观尺寸完全一致,只会让选定的字形指标更接近首选字体。
font-style: normal 请求直立字形,italic 请求字体中专门设计的斜体,oblique 则请求倾斜字形,还可以附带角度,例如 oblique 12deg。这里要区分“斜体”和“把字倾斜”:italic 往往会重新设计字母结构,oblique 通常是在直立轮廓基础上表现倾斜。
em { font-style: italic; }
.aside { font-style: oblique 10deg; }可变字体通常用 ital 轴切换斜体,用 slnt 轴控制倾斜角。注册字体时,@font-face 的 font-style 描述符也可以声明它支持的角度范围:
@font-face {
font-family: "Reading Variable";
src: url("/fonts/reading-variable.woff2") format("woff2");
font-style: oblique 0deg 20deg;
}font-stretch 用来选择同一家族中的窄体或宽体。它可以使用从 ultra-condensed 到 ultra-expanded 的关键词,也可以写百分比;100% 表示正常宽度,常见规范范围是 50% 到 200%。浏览器会选择字体已经提供的宽度字形或可变宽度轴,并不会像处理图片那样任意横向拉伸字形轮廓。
.condensed { font-stretch: 75%; }
.wide { font-stretch: expanded; }如果普通字体没有宽度变体,这条声明可能不会产生明显变化。可变字体通常通过 wdth 轴提供连续宽度。注册这类字体时,描述符 font-stretch: 75% 125% 是在告诉浏览器该资源覆盖的宽度范围。
字体家族缺少粗体、斜体、小型大写或上下标字形时,浏览器可能人工生成近似效果。这样至少还能看出强调关系,但字形设计、笔画细节和行高也可能随之走样。font-synthesis 可以指定允许浏览器合成哪些类型:
.brand-heading {
font-synthesis: none;
}
.article {
font-synthesis: weight style;
}可用关键字包括 weight、style、small-caps 和 position,none 表示全部禁止。在设置 none 之前,先确认真实的粗体和斜体文件已经加载。否则 <strong> 和 <em> 的语义虽然还在,视觉上的强调却可能看不出来。
具体策略可以按内容来分。正文可以允许有限合成,让强调关系不至于消失;对字形外观要求较严的品牌标题,应先注册所需字形,再关闭合成。这样处理的依据很直接:正文先保证阅读,品牌文字再保证外观准确。
font-variant 是一组字形替换属性的简写。声明写对了,不代表页面一定会出现变化;当前字体还必须包含对应的 OpenType 字形。
大写变体 font-variant-caps 支持 small-caps、all-small-caps、petite-caps、all-petite-caps、unicase、titling-caps。字体内置的小型大写会专门调整笔画粗细和比例,效果通常比直接缩小大写字母自然。
数字变体 font-variant-numeric 可以组合使用,分别控制数字的高度、宽度和特殊写法:
lining-nums / oldstyle-nums:齐线数字与旧式数字;proportional-nums / tabular-nums:比例数字与等宽数字;diagonal-fractions / stacked-fractions:斜线分数与堆叠分数;ordinal、slashed-zero:序数标记与带斜线零。.price-table {
font-variant-numeric: tabular-nums lining-nums;
}连字 font-variant-ligatures 控制普通、任意、历史和上下文连字,也支持对应的 no-* 反向值。normal 一般会保留普通连字,none 则关闭全部连字。某些代码字体会把 != 绘制成一个组合符号,这也是上下文替换。屏幕上的形状变了,源代码中的两个字符并没有改变。
替代字形 font-variant-alternates 支持 historical-forms、stylistic()、styleset()、character-variant()、swash()、ornaments()、annotation()。你可以通过 @font-feature-values 给数字索引起一个可读名称,不过浏览器和字体是否支持,仍要用实际页面验证。
东亚变体 font-variant-east-asian 可以请求 jis78、jis83、jis90、jis04、simplified、traditional、full-width、proportional-width、ruby 等字形。这些值只替换字形,不会翻译文字,也不会自动调整 ruby 注音的大小。
位置变体 font-variant-position 中的 sub 和 super,会请求字体内专门设计的上下标字形。它们比单纯缩小、移动普通字符更容易辨认,也不容易把行盒撑高。前提仍是浏览器和当前字体确实支持这些字形。
.editorial-title {
font-variant: small-caps common-ligatures oldstyle-nums;
}font-kerning 控制字距调整。auto 交给浏览器判断,normal 请求使用字体内置的字距数据,none 则关闭这项功能。字距数据针对特定字形组合,不等同于给所有字符统一增加 letter-spacing。字距和字间距还可能影响连字选择,所以测试时要使用页面里真正会出现的语言和字符组合。
font-feature-settings 可以直接开关底层 OpenType 特性。每个标签都必须由四个 ASCII 字符组成,并放在引号中;后面可以跟 on、off 或整数:
.special {
font-feature-settings:
"liga" on,
"smcp" 1,
"zero" on;
}"liga"、"liga" 1 和 "liga" on 表达的是同一个开关。部分特性还允许使用大于 1 的索引。常见标签有:"smcp" 表示小型大写,"tnum" 表示等宽数字,"onum" 表示旧式数字,"frac" 表示分数,"zero" 表示带斜线的零。
能用高级属性表达时,建议先用高级属性,例如用 font-variant-numeric: tabular-nums 设置等宽数字。遇到高级属性无法覆盖的特殊功能,再写 font-feature-settings。原因是它按整组值参与层叠:后面的声明会替换前面的整组标签,新增一个开关时很容易顺手关掉原有功能。
在 @font-face 中,同名描述符可以规定这个字形面默认启用或禁用哪些特性,font-variant 描述符也能为字形面设置变体。这些配置写在资源定义层;文字最终怎样绘制,仍由元素上的属性共同决定。
这个实验使用系统字体。如果打开某个选项后看不出变化,先检查当前字体有没有对应字形,不要立即把它判断为 CSS 语法错误。
可变字体可以在一个文件里保存连续变化的设计参数。常见注册轴包括 wght(字重)、wdth(宽度)、slnt(倾斜)、ital(斜体开关)和 opsz(光学尺寸)。这些标准轴通常都有对应的 CSS 高级属性,使用时先从这些属性开始:
.hero {
font-weight: 720;
font-stretch: 92%;
font-style: oblique 8deg;
font-optical-sizing: auto;
}对于字体自定义轴,或者高级属性无法表达的设置,可以用 font-variation-settings 通过四字符标签和数值直接操作轴:
.display {
font-variation-settings:
"wght" 720,
"wdth" 92,
"GRAD" 50;
}轴标签必须加引号,长度正好是四个字符,并且区分大小写。数值超出字体支持范围时,通常会被限制在边界值。font-variation-settings 还可能覆盖高级属性对同一轴的控制,所以同一个轴尽量只交给一处设置。
如果字体支持 opsz 轴,font-optical-sizing: auto 会让它根据实际字号调整字形细节。小字号通常会扩大开口、加强笔画,大字号则能保留更细的细节;none 会关闭自动调整。这里改变的是字形针对阅读尺寸的设计,不是简单缩放,也和 font-size-adjust 处理的回退字体尺寸不同。

当前系统字体即使没有连续可变轴,你仍可以用这个实验比较字重、宽度、倾斜和字号对应的声明。没有精确字形时,浏览器会从已有字形中尽量匹配。
font 简写可以放入 font-style、font-variant-caps 中的 small-caps、font-weight、font-stretch、font-size、line-height 和 font-family。其中字号和家族必须出现,而且字号要写在家族之前;如果要写行高,就紧跟字号并用 / 分隔:
h1 {
font: italic small-caps 700 condensed 2rem/1.15 "Site Sans", sans-serif;
}前面的可选值在顺序上有一定弹性,尾部的 font-size[/line-height] font-family 则不能拆开。font-stretch 的关键词可以进入简写,但百分比在部分语法版本和实现中限制较多。需要兼容多个环境时,把百分比宽度单独写出来更容易确认结果。
使用简写时,最容易漏掉的是它会重置没有写出的项目:
.title {
font-style: italic;
font-weight: 700;
font: 2rem "Site Sans", sans-serif;
}执行到最后一行后,前面设置的 font-style 和 font-weight 都会回到 normal。font 还会把一些无法放进简写的属性重置为初始值,包括 font-feature-settings、font-kerning、font-optical-sizing、font-size-adjust、大多数 font-variant-* 和 font-variation-settings。如果两类声明要同时使用,先写 font,再补充这些高级属性。
font 也接受系统字体关键字,例如 caption、icon、menu、message-box、small-caption 和 status-bar。一个关键字就能取得系统界面对应的一整套字体值:
button {
font: caption;
font-size: 1em;
}系统字体会一次设置整组值,后面仍可以用长属性覆盖其中一项。它在不同操作系统和浏览器里的外观并不相同,因为选择的就是当前平台对应的界面字体。
当声明的字体文件没有被使用时,只看 font-family 往往找不到原因,还要把浏览器的匹配顺序一起考虑。它先从字体家族列表中找资源,再按照字形特征逐步筛选。我们可以把这个过程理解为:
先从 font-family 列表的第一项开始,在本机字体、浏览器字体与 @font-face 注册资源中寻找可用家族。
在家族内部先匹配宽度,再匹配样式,然后匹配字重;现有字形不完全吻合时,会按规范选择最接近的可用字形。
浏览器再处理字号、默认开启的字体功能、@font-face 描述符、font-variant 与 font-feature-settings 等层级带来的特性选择。

字体特性也按层级生效。字体本身先提供默认功能;@font-face 中的 font-variant 和 font-feature-settings 描述符负责资源层设置;字距等属性可能继续影响特性选择;之后才是元素上的 font-variant-*,底层 font-feature-settings 会处理具体标签。多层设置互相冲突时很难排查,因此同一种特性尽量集中在一个层级控制。
调试字体时,CSS 声明只是线索之一。开发者工具中的“已渲染字体”可以确认某段文字实际使用了哪个文件,网络面板可以检查 WOFF2 是否下载、是否命中缓存。再切换到离线或慢网模式,就能看到回退字体和 font-display 策略是否按预期工作。
字体配置准备上线时,可以按下面的顺序逐项检查:
unicode-range。swap、fallback 或 optional,选完后要在慢网下实际观察。size-adjust 和度量覆盖描述符校准回退字体,同时记录测试所用的字体和平台。font-variant-*、font-weight、font-stretch 控制标准注册特性,底层 settings 留给无法直接表达的需求。font 写完之后,再声明字距、光学尺寸、变体和可变轴,避免它们被简写重置。检查时也可以反过来找这些常见错误:给通用族加了引号;把不同粗细文件注册成不同家族;以为 font-stretch 能压扁任意字体;把 font-size-adjust 当成普通缩放;为了打开一个特性而覆盖整组 font-feature-settings;禁止字体合成后却没有提供真实粗体。网络和平台也要纳入测试,只在高速网络、单一系统上验收,很难发现字体加载和回退的问题。
回头看整套字体配置,可以把它放进三个层次。@font-face 说明有哪些资源以及这些资源能提供什么字形,元素上的字体属性说明想要哪种字形,浏览器的匹配和回退机制再决定最终用哪个文件绘制。
这样分类后,各个属性的职责就清楚了。font-family 排列候选家族,font-weight、font-style 和 font-stretch 参与字形面匹配,font-display 处理加载期间的显示方式。font-variant-* 与 font-feature-settings 选择具体字形特性,font-variation-settings 操作可变轴,度量覆盖则减少字体切换时的布局变化。
实际配置时,我们可以先确认一件事:即使网页字体没有加载,内容仍然能读。做到这一点后,再根据需要校准回退字体、控制加载过程并加入字形特性。字体可以改变页面气质,但阅读不应该被某个字体文件卡住。
若选中字形缺少某个字符,浏览器会为这个字符继续寻找能覆盖该码位的字体,因此一个段落甚至一个词内可能混用多个字体。
所有具体家族均无法满足时,浏览器落到最后的通用族默认字体,优先保证内容仍能显示。