系统字体的 font-weight 500 兼容性处理
简体中文用系统字体时,font-weight: 500 在苹方 SC 上可以用到 Medium。在 Windows 上,如果命中的是微软雅黑或微软雅黑 UI,500 会落到 Regular,强调文字就和 400 的正文一样粗。Android 则要看版本、机型和当前的系统字体。
| 简体中文实际用到的字体 | 请求 500 时 |
|---|---|
| iOS、macOS 上的苹方 SC | 有 Medium。Apple 列出 Ultralight、Thin、Light、Regular、Medium 和 Semibold 六个字面 |
| Windows 上的微软雅黑或微软雅黑 UI | 有 Light、Regular、Bold,缺 Medium。在本次 Windows Chrome 实测中,500 选到 Regular |
| Android 的中文回退字体 | 旧版 AOSP 的简体中文 Noto Sans CJK 回退只登记 Regular。Android 15 起引入可变版,部分厂商也有自己的多字重系统字体 |
Apple 和 Microsoft 的字体清单分别见 苹方 SC 的字面列表 和 微软雅黑的字重列表。Android 的配置可对照 AOSP Android 11 的字体表 和 Android 15 的 CJK 可变字体说明。
用同一组汉字看 400、500 和 700
对照页把 system-ui, sans-serif 设在 html 上,再用 16px、同样的字色和背景排三行「窗边的书页」,分别请求 400、500 和 700。为了在同一个浏览器里比较四列,我在 Windows Chrome 157 里保留了微软雅黑 UI 的系统回退,把两个 Android 镜像里的简体中文 Noto Sans CJK,以及 Apple 官方目录里的苹方 SC 作为字体文件加载。
图中的文字由浏览器输出后转成 SVG 路径。微软雅黑 UI 和 Android 11 的 Noto Sans CJK 都把 500 画得和 400 一样。Android 16 的 Noto Sans CJK 有 wght 轴,三档都能画出不同粗细。两个 Android 镜像里的系统 WebView 也观察到同样的字重差异,可以对照 设备实拍 和 系统字体配置。
苹方 SC 用的是 Apple 提供的 21.0d1e1,六个字面按文件里的 100–600 注册。在 Windows Chrome 里请求 400、500、700,实际分别用了 Regular、Medium、Semibold。最后一档和直接请求 600 逐像素相同,关掉粗体合成后也是同一幅图。浏览器选到的是现有的 Semibold。字面名称和像素校验值见 浏览器记录,原始画面见 四列 2× 截图。
苹方这一列验证的是本机 Chrome 对指定字体文件的匹配。要观察 iOS 或 macOS 实际选中的系统字面,可以在目标设备打开 系统字体对照页。
CSS 显示 500,字为什么没变粗
这里的 500 是 CSS 的计算值。浏览器还要在字体家族里选一个具体字面,也就是 Regular、Medium 或 Bold 这样的字形版本。选中 500 那行,在 Chrome DevTools 的 Computed 面板往下看 Rendered Fonts,就能看见实际绘制这几个字的字体。英文和中文可能分别命中不同字体,检查时要选中出现差异的那几个汉字。Windows 的计算值和实际字体保存在 浏览器实测记录。
对请求值 500,CSS 的匹配规则会先找精确值,再往较细的方向找,最后才考虑比 500 更粗的字重。手里只有 400 和 700 时,先找到的就是 400。请求 600 则先往较粗的一侧找。我在同一环境补测了 600,它和 700 都用了 Bold。完整的匹配顺序见 CSS Fonts 4 的字体匹配算法。
现在的 Android 手机也可能用厂商自带的字体。例如小米的 MiSans 有 Medium,荣耀的 HONOR Sans 支持包括 Medium 在内的连续字重,OPPO 和 vivo 也分别提供 OPPO Sans 和 vivo Sans。厂商字体能否在网页里响应 500,还要看手机的字体设置和浏览器或 WebView 实际命中的字面。
打开合成也补不出 Medium
font-synthesis-weight 的初始值是 auto,允许浏览器在缺少粗体时模拟粗体,定义见 CSS Fonts 4 的粗体合成规则。
我用只声明了 400 的悠哉 Regular 做了对照。在同一版本的 Chrome 里,500 和 400 的截图像素相同,600、700 和 800 的截图也彼此相同。把 700 的合成关掉后,画面回到了 Regular。合成开关只决定浏览器能不能模拟粗体,粗到什么程度由浏览器决定。
用 UA 给根节点设置强调字重
以前做跨端页面时,设计稿用 400 排正文,用 500 标出重点。在命中苹方 Medium 的设备上,差别刚好。项目首先要保证看得出强调,所以当时在根节点给 --bold 一个默认值,再让强调元素读取这个变量。
html {
--bold: 500;
font-family: system-ui, sans-serif;
font-weight: 400;
}
strong {
font-weight: var(--bold);
}
初始化脚本读 UA。对当时需要兼容的 Windows 和 Android,把根节点的 --bold 改成 700。后代继承这个自定义属性,正文仍按 400 排,strong 则读到新的强调字重。
if (/Windows NT|Android/i.test(navigator.userAgent)) {
document.documentElement.style.setProperty('--bold', '700');
}
把脚本放在首次渲染前执行,可以避免先画出 500 再切到 700。这段判断适用于当时已验收的平台组合。它把 Android 整体提到了 700,今天遇到能渲染 500 的设备,也会请求较粗的一档。Windows 若沿用当前的微软雅黑,则仍需准备兼容样式。
纯 CSS 先找已知的 Medium,找不到就请求 700
如果兼容目标是「能用苹方 Medium 就用它,其余情况至少看得出加粗」,可以把这个已知的本地字面放在系统字体栈前面。这里用 @font-face 给苹方 Medium 起一个别名,并声明它响应 700 请求。
@font-face {
font-family: 'Local CJK Medium';
src: local('PingFangSC-Medium'), local('PingFang SC Medium');
font-weight: 700;
}
html {
--system-font: system-ui, sans-serif;
font-family: var(--system-font);
font-weight: 400;
}
strong {
font-family: 'Local CJK Medium', var(--system-font);
font-weight: 700;
}
命中本地字面时,浏览器匹配的是别名的 700,绘制时用的仍然是 Medium 的轮廓。加载不到时,浏览器就向后面的系统字体请求 700。本地字面的查找方式见 CSS Fonts 4 的字体来源描述符。
我在 Windows Chrome 上核对了两条路径。找不到苹方时,结果和直接写系统字体 700 一样。把别名的来源换成这台机器能加载的游ゴシック Medium 后,声明为 700 和声明为 500 的两个别名画出来一样。苹方是否命中需要在目标 Apple 设备上确认。
维护这段 CSS 时,要知道哪些 Medium 字面值得列进去。比如某台 Android 的系统回退能画出 500,但它的字面名没列在 local() 里,页面还是会请求系统字体的 700。如果设计要求中英文各自沿用原本的系统字体,也要分开检查,显式指定的本地字体可能同时覆盖两种文字。
细描边可以模拟轻强调
如果觉得 700 跳得太多,可以在 400 上加一条同色细描边。下图最后一行就是在 Windows 上加 0.1px 的效果。
html {
font-family: system-ui, sans-serif;
font-weight: 400;
}
strong {
font-weight: inherit;
-webkit-text-stroke: 0.1px currentColor;
}
用这几个汉字比较,按截图亮度估算的笔画覆盖量在 0.1px 和 0.15px 时都介于 Regular 与 Bold 之间,两种像素密度下都如此。加到 0.3px,在 1× 下就略超过了 Bold。描边沿字形边缘绘制,笔画之间的空隙也会变窄,具体见 WHATWG 的文字描边定义。先从 0.1px 试,再用目标字号看「窗」这样的密集字和真实段落,尤其要在 1× 下看一次。Windows 2× 实拍
纯 CSS 可以给强调文字统一用细描边。要在缺某个 Medium 时才描边,就需要让字面加载结果影响元素的样式。local() 只参与字体匹配,@supports (font-weight: 500) 只检查声明能否被浏览器接受。这个查询的范围见 CSS Conditional Rules 的支持查询。
用 JS 探测指定的本地 Medium
已知目标字面时,可以直接尝试加载它。CSS 先给强调文字用系统字体和细描边。加载成功后,脚本注册这个 Medium,并用一个 class 把强调切换到对应字面。
html {
--system-font: system-ui, sans-serif;
--bold: 400;
--bold-stroke: 0.1px;
font-family: var(--system-font);
font-weight: 400;
}
html.has-local-medium {
--bold: 500;
--bold-stroke: 0px;
}
strong {
font-family: 'App Local Medium', var(--system-font);
font-weight: var(--bold);
-webkit-text-stroke: var(--bold-stroke) currentColor;
}
async function preferLocalMedium() {
const medium = new FontFace('App Local Medium', 'local("PingFangSC-Medium"), local("PingFang SC Medium")', {
weight: '500',
});
try {
await medium.load();
} catch {
return;
}
document.fonts.add(medium);
document.documentElement.classList.add('has-local-medium');
}
void preferLocalMedium();
用 document.fonts.check('500 16px system-ui') 查不到这个结果。它检查绘制这段样式是否还要等待字体加载,能用 Regular 绘制时也可以返回 true,定义见 CSS Font Loading 的字体检查规则。
脚本加载后切换字体家族,字宽和换行可能跟着变。首屏排版需要稳定时,可以统一用细描边,或者接受纯 CSS 方案的 700 回退。
总结
系统字体下的 500 只是一档请求。实际命中 Regular 时,即使 CSS 已经生效,轻强调也可能消失。要沿用系统字体并优先保证层级,可以给确认过的 Medium 写一个 local() 别名,其余情况请求 700。如果 700 太重,细描边可以作为统一样式,也可以由 JS 在指定的 Medium 加载失败时保留。设计要求各端都得到真实的 400 → 500 时,则要提供覆盖这两档、且覆盖实际文字的字体。