3. Web环境下的偏移:CSS line-height与实际触摸区域、浏览器默认字体回退机制、@font-face加载导致的FOUT/FOIT
Web环境下的字体触摸偏移,说白了就是「你以为用户点的是这里,结果他点到了隔壁」。这个问题在移动端尤其要命。我排查过不少线上反馈,最后发现根子往往出在三个地方:line-height 的计算方式、字体回退时的尺寸突变、以及自定义字体加载时的闪烁。咱们一个一个拆。
3.1 CSS line-height 与实际触摸区域的「错位」
先问个问题:一个按钮的高度是 44px,文字字号是 16px,line-height 设了 1.5。那实际的可点击区域到底有多大?
很多人以为就是按钮的 height。不对。line-height 决定了文字行盒的高度,而这个行盒才是浏览器用来计算点击命中区域的基础。我见过一个案例:设计师给了一个 40px 高的按钮,开发设了 line-height: 40px,看起来完美居中。但用户反馈「点按钮没反应,要点偏上一点才行」。一查,按钮里用了 icon + 文字,icon 的字体基线把行盒撑高了,实际触摸区域比视觉区域上移了 4px。
怎么排查?我习惯用 Chrome DevTools 的「检查元素」功能,把鼠标悬停在文字上,看高亮区域。那个高亮区域就是实际的行盒范围。如果它和按钮的视觉边界对不上,那触摸偏移就来了。
| line-height 值 | 行盒高度(字号16px) | 触摸偏移风险 | 常见场景 |
|---|---|---|---|
| 1.0 (16px) | 16px | 低,但文字拥挤 | 图标字体、单行文本 |
| 1.5 (24px) | 24px | 中,上下留白不均 | 正文、段落 |
| 2.0 (32px) | 32px | 高,触摸区域明显偏移 | 标题、按钮(需谨慎) |
padding: 12px 16px; font-size: 16px; line-height: 1.2;。
3.2 浏览器默认字体回退机制
你指定了 font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif。但用户设备上没有 PingFang SC,也没有微软雅黑,最后 fallback 到了系统默认的 sans-serif。这时候问题来了——不同字体的度量(metrics)不一样。
举个例子:PingFang SC 的 x-height 比微软雅黑高,同样的字号,PingFang 看起来更大。一旦回退到微软雅黑,文字变小了,行盒高度也变了。如果按钮高度是写死的,那文字上下就会出现多余的空白,触摸区域自然就偏了。
我记得有一次做 H5 活动页,设计稿用的苹方,开发没注意 fallback 顺序,写成了 'Microsoft YaHei', 'PingFang SC', sans-serif。结果大部分 iOS 用户正常,但部分 Android 用户因为系统字体优先级问题,实际渲染的是 Noto Sans CJK,行高差了 3px,导致「立即参与」按钮在部分机型上点不到。
推荐的做法是给每个 fallback 字体单独设置 line-height:
/* 不推荐 */
.button {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
line-height: 1.5;
}
/* 推荐:用 CSS 变量或 @supports 做差异化 */
.button {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
line-height: 1.5;
}
/* 针对不同字体微调 */
@supports (font-family: 'PingFang SC') {
.button { line-height: 1.4; }
}
3.3 @font-face 加载导致的 FOUT/FOIT
FOUT(Flash of Unstyled Text)和 FOIT(Flash of Invisible Text)是 @font-face 加载时的两个老问题。简单说:
- FOUT: 字体还没加载完,浏览器先用 fallback 字体渲染文字。等自定义字体加载完,再替换。这个「闪一下」的过程,会导致文字尺寸、行高突变,触摸区域也跟着变。
- FOIT: 浏览器干脆不显示文字,等字体加载完再渲染。这段时间用户看不到文字,但触摸区域还在(通常是 0 或极小),用户点上去没反应。
我遇到过最离谱的一次:一个电商 App 的 Web 版,用了自定义字体做品牌标题。字体文件 800KB,加载花了 3 秒。这 3 秒内,标题区域是空的(FOIT),但按钮布局已经渲染好了。用户以为那里有字,点了一下,结果点到了下面的商品卡片。这就是典型的 FOIT 导致的触摸偏移。
font-display: swap 或 font-display: fallback 来控制加载行为。swap 会先显示 fallback 字体(FOUT),等自定义字体加载完再替换。虽然会闪一下,但至少用户能看到文字,触摸区域也是正常的。
具体配置:
@font-face {
font-family: 'MyCustomFont';
src: url('/fonts/MyCustomFont.woff2') format('woff2');
font-display: swap; /* 或 fallback */
font-weight: 400;
font-style: normal;
}
但 swap 也有坑。如果 fallback 字体和自定义字体的度量差异太大,替换时文字会「跳一下」,触摸区域也会跟着跳。我建议在 @font-face 里用 size-adjust 和 ascent-override 等属性做度量对齐:
@font-face {
font-family: 'MyCustomFont';
src: url('/fonts/MyCustomFont.woff2') format('woff2');
font-display: swap;
size-adjust: 95%; /* 缩小 5%,让 fallback 和自定义字体视觉大小接近 */
ascent-override: 90%; /* 调整上坡度,减少行高突变 */
}
font-display: optional,配合预加载(<link rel="preload">)。这样字体加载失败也不影响布局,但需要确保字体文件足够小(< 100KB)。
3.4 本章知识体系
下面这张图把三个核心问题串起来了。你可以看到,line-height、字体回退、@font-face 加载,最终都指向同一个结果——触摸区域偏移。
嗯,这三个问题单独看都不复杂,但一旦组合起来,排查起来就头疼了。我建议你在项目里建立一套「字体触摸偏移检查清单」:每次上线前,至少用真机测 3 台不同系统的设备,重点看按钮、链接、表单输入框这些高频交互元素。如果发现点击区域和视觉区域对不上,优先检查 line-height 和字体回退路径。