你写的 clamp() 流体排版,在用户放大浏览器时会悄悄缩水——CSS progress() 把这件事彻底算清楚了
做响应式排版,你是不是已经形成了肌肉记忆:先定一套最小字号,再用 clamp() 套个线性插值,最后在几个断点处微调?这套做法在桌面端看着挺稳,但真正让问题浮出水面的,往往是用户的浏览器设置。
问题:clamp() 的 rem 断点,会背叛放大字体的用户
当用户在浏览器里把默认字号从 16px 调到 20px 时,页面里所有 rem 单位都会等比放大,这本来是好事。但如果你之前为了“方便”,把 clamp() 的起止断点写死成了 rem,问题就来了:
h1 {
font-size: clamp(1.5rem, 1rem + 2vw, 3rem);
}
这里 1rem + 2vw 的公式,本质上是在做“视口宽度 → 字号”的线性映射。可一旦 rem 基准变了,这个映射的起点和终点都会被一起放大,结果就是:用户明明放大了字体,但标题在移动端反而显得更“挤”。
这不是 clamp() 的 bug,而是 rem 和视口单位混用时的天然缺陷。把视口断点换算成 rem 的那一刻,你就已经把“用户偏好”和“设计意图”绑在了一起,没法单独控制。
方案:用 progress() 把映射逻辑还给浏览器
CSS progress() 的思路更直接:它不做单位换算,只做“当前值在范围里的位置”计算,返回一个 0 到 1 之间的无单位数字。你再把这个数字拿去乘你想要的范围,就得到了和用户偏好解耦的响应式字号。
:root {
--min-viewport: 320px;
--max-viewport: 1200px;
--min-size: 1rem;
--max-size: 1.5rem;
}
h1 {
font-size: calc(
var(--min-size) +
progress(100vi, var(--min-viewport), var(--max-viewport)) *
(var(--max-size) - var(--min-size))
);
}
这段代码的意思是:不管用户把默认字体设成多大,h1 都会在 320px 到 1200px 的视口范围内,从 1rem 平滑过渡到 1.5rem。rem 负责尊重用户偏好,vi 负责响应视口,progress() 负责把两者桥接起来。
进阶:把同一套比例复用在间距、行高、甚至颜色上
因为 progress() 返回的是无单位数值,你不仅可以算字号,还能直接喂给 rgb()、opacity、letter-spacing 等属性。比如一套基于视口宽度的“昼夜渐变”背景:
.hero {
background: rgb(
calc(255 * progress(100vi, 320px, 1200px)),
calc(255 * progress(100vi, 320px, 1200px)),
255
);
opacity: calc((progress(100vi, 320px, 1200px) / 2) + 0.5);
}
视口越宽,背景越亮;视口越窄,整体越偏深蓝。全程不需要 JavaScript,也不需要预计算断点。
注意:Firefox 还没支持
截至今天,progress() 在 Chrome 138+、Edge 138+、Safari 26+ 已可用,但 Firefox 仍不支持。如果你要做渐进增强,建议把 progress() 的结果回退到 clamp():
h1 {
font-size: clamp(1rem, 1rem + 2vw, 1.5rem);
font-size: calc(
var(--min-size) +
progress(100vi, var(--min-viewport), var(--max-viewport)) *
(var(--max-size) - var(--min-size))
);
}
下一步
如果你团队里还在用 rem 断点 + clamp() 的线性方案,建议先在一个非关键页面试水 progress()。重点看两个场景:用户把浏览器默认字体调到 125% 时,标题是否还保持可读;以及移动端横屏时,字号过渡是否平滑。把这两个场景测顺了,再往全局推广。
评论区
登录后可评论。