设计稿给出了两个明确要求:在 320px 宽的屏幕上,标题应为 16px;在 1440px 宽的屏幕上,应为 32px。设计意图很清楚,实现方式却没有那么直接。你可以用媒体查询在两个字号之间跳变,也可以随手写下 clamp(1rem, 2.5vw, 2rem)。后者看起来合理,却未必真正经过设计稿给出的两个目标点。

可靠的流体尺寸不需要靠猜。两组“视口宽度—属性值”可以确定一条直线,而这条直线可以转换为带固定边界的 CSS 视口表达式。

打开 CSS Clamp 计算器 →

本文会完整推导公式,说明如何验证结果,覆盖正向与反向范围,解释何时选择 rem 或 px,并给出 JavaScript、Python 与 Bash 的轻量生成代码。目标不是从计算器里复制一串神秘数字,而是得到一段能在代码评审中解释清楚的 CSS。

clamp() 到底做了什么

CSS clamp() 接收三个以逗号分隔的计算式:

property: clamp(minimum, preferred, maximum);

浏览器会计算中间的首选表达式,并将结果限制在上下边界之间。其形式化定义等价于:

max(minimum, min(preferred, maximum))

这个定义很重要,因为中间值并不是“默认值”,而是浏览器根据当前视口实时求值的响应式规则。只有当计算结果超出允许范围时,两侧边界才会接管。

例如:

font-size: clamp(1rem, 0.7143rem + 1.4286vw, 2rem);

它可以分为三个区间理解:

  1. 视口较窄时,首选值会小于 1rem,因此采用最小值。
  2. 位于两个端点之间时,中间表达式随视口线性变化。
  3. 视口较宽时,首选值会超过 2rem,因此采用最大值。

MDN 的 clamp() 文档将它列为现代浏览器广泛支持的能力。比较函数的正式行为定义在 CSS Values and Units Level 4 中。

为什么 clamp(1rem, 2.5vw, 2rem) 通常并不完整

单独使用视口单位有时也能解决问题,但它只有斜率,没有可以独立选择的截距,因此通常无法精确经过任意两个设计目标点。

看这段常见写法:

font-size: clamp(1rem, 2.5vw, 2rem);

在 320px 视口下,2.5vw 等于 8px,所以结果被最小值限制为 16px。在 1440px 视口下,2.5vw 等于 36px,结果被最大值限制为 32px。它最终确实会碰到两个边界,但真正的流体区间是 640px 到 1280px,而不是 320px 到 1440px。在 880px 时,它得到 22px;如果在设计端点之间做直线插值,此时应该是 24px

缺少的部分就是截距。为了精确命中两个端点,首选表达式通常需要一个固定项和一个视口项:

fixed-intercept + viewport-slope

这正是直线方程 y = mx + b

从两个端点推导公式

先定义四个输入:

  • v₁:最小视口宽度
  • v₂:最大视口宽度
  • s₁:最小视口对应的属性值
  • s₂:最大视口对应的属性值

斜率等于属性值变化量除以视口变化量:

m = (s₂ - s₁) / (v₂ - v₁)

截距表示把这条直线延伸到视口宽度为零时的属性值:

b = s₁ - m × v₁

CSS 中的 1vw 等于视口宽度的百分之一,因此把“每像素变化量”的斜率乘以 100,就得到 vw 系数:

preferred = b + (m × 100)vw

完整示例:从 320px 时的 16px 变化到 1440px 时的 32px

先计算斜率:

m = (32 - 16) / (1440 - 320)
  = 16 / 1120
  = 0.0142857143

再计算截距:

b = 16 - 0.0142857143 × 320
  = 11.4285714px

将斜率转换为 vw:

m × 100 = 1.42857143

因此,使用 px 的结果是:

font-size: clamp(16px, 11.4286px + 1.4286vw, 32px);

如果根字体大小为 16px,把固定长度都除以 16 即可。vw 系数不变:

font-size: clamp(1rem, 0.7143rem + 1.4286vw, 2rem);

验证两个端点

在 320px 视口下,1vw 等于 3.2px:

11.4286px + 1.4286 × 3.2px ≈ 16px

在 1440px 视口下,1vw 等于 14.4px:

11.4286px + 1.4286 × 14.4px ≈ 32px

端点检查可以发现绝大多数手算错误,也能说明为什么应该最后再做四舍五入。如果先把斜率取整,再据此计算截距,累积误差可能让某个端点出现肉眼可见的偏移。

实用的计算器工作流

ZeroTool CSS Clamp 计算器让输入方式尽量贴近设计交付内容:

  1. 选择 font-sizepaddingmargingapwidth 或自定义属性。
  2. 输入最小和最大视口宽度,单位为像素。
  3. 输入两个视口端点所要求的属性值。
  4. 选择 px 或 rem 输出;选择 rem 时,确认根字体大小。
  5. 查看五个采样值,并拖动预览滑块。
  6. 复制完整 CSS 声明,或只复制生成的数值。

曲线有意保持为线性,因为两个点只能确定一条直线。采样表会展示中点和四分点,方便你发现那些数学上正确、设计感受却不合适的范围。

计算器还处理了普通代码片段经常漏掉的三种情况:

  • **反向范围:**视口越宽,属性值越小。
  • **固定范围:**两个端点相同,结果简化为一个固定长度。
  • **负值:**仅对外边距和自定义属性开放,因为这些场景下负长度可能有明确含义。

计算和状态保存都在浏览器本地完成。“重置”会恢复默认值,并删除保存的表单状态。

选择 rem 还是 px

公式可以混合使用不同单位。这不是权宜之计,CSS 数学函数本来就支持组合兼容的长度单位。

文字和需要跟随用户设置的系统优先使用 rem

使用 rem 输出时,最小值、最大值和固定截距都会相对于根字体大小:

.hero-title {
  font-size: clamp(1rem, 0.7143rem + 1.4286vw, 2rem);
}

这样可以让表达式的一部分继续响应用户的字体偏好。对于排版,以及需要跟随字体系统缩放的间距 token,rem 通常是更合适的起点。

换算仍然依赖明确的根字体大小。如果项目设置了 html { font-size: 62.5%; },却让计算器按 16px 换算,得到的 rem 就不适合该项目。计算器中的根字体输入必须与设计系统实际采用的基准一致。

端点确实是固定尺寸时使用 px

如果一个视觉尺寸必须落在精确的 CSS 像素端点上,px 输出很实用:

.content-shell {
  padding-inline: clamp(20px, 12px + 2.5vw, 48px);
}

CSS 像素并不等于物理设备像素,但在 CSS 布局中仍然是固定长度。对于文字,它与用户字体偏好的联系通常不如 rem。

vw 项始终跟随视口

选择 rem 并不会让整个表达式都变成字体相对值。中间表达式仍然包含 vw,因此会随视口宽度变化。固定项与视口相对项对各种用户设置的响应并不完全相同,这正是缩放与文字放大测试仍然不可省略的原因。

如果需要单独核对端点换算,可以使用 CSS 单位转换器

反向流体范围

有些属性应该随屏幕变宽而减小。例如,移动端紧凑卡片可能需要更大的垂直内边距来保证触控舒适度,而高密度桌面布局反而需要更小的间距。让 320px 对应 32px、1440px 对应 16px,就会得到负斜率:

padding-block: clamp(1rem, 2.2857rem - 1.4286vw, 2rem);

在较小视口下,首选表达式求值为 2rem;在较大视口下,求值为 1rem

这里有一个容易忽略的实现细节:CSS 仍然要求较小的边界在前,较大的边界在后。缩放方向反转,并不意味着应该生成 clamp(2rem, ..., 1rem)。上下边界仍按 1rem2rem 排列,方向由负的 vw 系数表达。

CSS 规范规定,当作者给出相互冲突的边界时,最小值参数优先。先对边界排序可以避开这个陷阱,让表达式按预期工作。

固定值也是正确答案

如果两个端点都是 16px,斜率就是零。理论上可以写成长形式:

font-size: clamp(1rem, 1rem + 0vw, 1rem);

但它没有传达任何额外信息。最小且正确的结果是:

font-size: 1rem;

计算器应该简化这种情况,而不是假装每对输入都需要响应式公式。设计系统中也应遵循同样原则:只在插值确实服务于布局时使用流体 token。

流体字号不等于自动满足无障碍要求

流体排版解决的是视觉插值,并不会自动满足无障碍规范。

WCAG 2.2 成功标准 1.4.4要求文字在特定例外之外,可以放大到 200%,且不丢失内容或功能。MDN 的 clamp() 无障碍建议提出:使用相对长度作为最大值,并让它至少达到最小值的两倍,可以避免过早限制文字增长。

这个比例是有用的提醒,不是合规证书。完整测试必须观察实际行为:

  1. 把页面缩放到 200%。
  2. 确认文字仍可阅读,没有控件消失。
  3. 检查标签是否换行,而不是被裁切。
  4. 确认固定高度容器没有截断内容。
  5. 在放大文字后重新测试窄屏布局。

W3C G179 技术说明关注的是同一个结果:文字变大后,内容与功能仍然必须可用。

当字号范围小于二比一时,ZeroTool 计算器会显示一条提示。它不会阻止生成 CSS,因为不同产品有不同约束,只有把结果放进真实组件测试,才能判断是否合适。

常见错误及其原因

1. 把中间值当作平均值

首选值是表达式,不是最小值与最大值的平均数。clamp(1rem, 1.5rem, 2rem) 这样的固定中间值不会响应视口宽度,结果始终是 1.5rem

2. 使用单独的 vw,却不检查端点

2vw 可能在某个截图宽度下看起来不错,却在其他宽度全部偏离设计目标。发布前应计算截距,或者至少验证两个端点。

3. 过早舍入斜率

最终 CSS 可以只保留四位小数,但计算过程应保持完整精度,直到序列化输出时再舍入。过早舍入会移动截距,导致端点验证失败。

4. 省略 +- 两侧的空格

CSS 数学语法要求二元加减运算符两侧存在空白。正确写法是:

clamp(1rem, 0.7143rem + 1.4286vw, 2rem)

而不是:

clamp(1rem, 0.7143rem+1.4286vw, 2rem)

5. 让视口区间为零

如果最小与最大视口宽度相同,斜率计算就会除以零。此时应判断自己是否真正需要固定值,或者重新输入两个不同的视口端点。

6. 假设所有项目的根字体都是 16px

16px 是常见默认值,不是所有项目都遵守的契约。把 px 端点转换为 rem 前,先检查根样式与面向用户的字体设置。

7. 在容器驱动的组件里使用视口插值

即使桌面视口很宽,其中的卡片仍可能很窄。vw 响应的是整个页面,而不是卡片本身。对于可独立复用的组件,应考虑手写的容器查询与容器相对单位是否更合适。本计算器明确生成基于视口的 CSS。

8. 把所有 token 都改成流体值

边框、图标描边、基线网格等精确细节通常更适合稳定数值。是否流体化应该是设计决策,而不是默认重构动作。

构建一套小型字号与间距系统

端点获得确认后,可以把结果存为自定义属性,避免把公式散落在各个组件中:

:root {
  --font-body: clamp(1rem, 0.9643rem + 0.1786vw, 1.125rem);
  --font-title: clamp(2rem, 1.5714rem + 2.1429vw, 3.5rem);
  --space-section: clamp(3rem, 2.1429rem + 4.2857vw, 6rem);
  --space-card: clamp(1rem, 0.8571rem + 0.7143vw, 1.5rem);
}

body {
  font-size: var(--font-body);
}

h1 {
  font-size: var(--font-title);
}

.section {
  padding-block: var(--space-section);
}

按用途命名 token,在设计系统文档中记录端点假设,并把生成值集中管理。公式确定后,可以用 CSS 变量生成器整理最终的 :root 代码块。

不要在缺少设计评审的情况下,仅凭一组端点就生成整套模块化字号阶梯。线性插值回答的是“这一个 token 如何在两个点之间变化”,不会替你决定所有标题层级是否应该共享同一个比例或端点。

用 JavaScript 生成 clamp 值

浏览器端计算器最适合一次性工作。对于构建工具或 token 流水线,同样的计算足够精简,不需要增加依赖:

function clampFromPoints({
  minViewport,
  maxViewport,
  minValue,
  maxValue,
  unit = 'rem',
  root = 16,
}) {
  if (!(maxViewport > minViewport)) {
    throw new Error('maxViewport must be greater than minViewport');
  }
  if (unit === 'rem' && !(root > 0)) {
    throw new Error('root must be greater than zero');
  }

  const round = value => Number(value.toFixed(4));
  const length = px =>
    unit === 'rem' ? `${round(px / root)}rem` : `${round(px)}px`;

  if (Math.abs(maxValue - minValue) < 1e-9) {
    return length(minValue);
  }

  const slope = (maxValue - minValue) / (maxViewport - minViewport);
  const interceptPx = minValue - slope * minViewport;
  const intercept = unit === 'rem' ? interceptPx / root : interceptPx;
  const vw = slope * 100;
  const sign = vw < 0 ? '-' : '+';
  const lower = Math.min(minValue, maxValue);
  const upper = Math.max(minValue, maxValue);

  return `clamp(${length(lower)}, ${round(intercept)}${unit} ${sign} ${round(Math.abs(vw))}vw, ${length(upper)})`;
}

console.log(clampFromPoints({
  minViewport: 320,
  maxViewport: 1440,
  minValue: 16,
  maxValue: 32,
}));
// clamp(1rem, 0.7143rem + 1.4286vw, 2rem)

计算时保留未舍入数值,只在输出 CSS 时格式化。如果 token 流水线把小数作为字符串处理,应至少为两个端点、反向范围和固定范围添加测试。

用 Python 生成相同结果

Python 版本适合放进设计 token 脚本或静态站点构建步骤:

def clamp_from_points(v_min, v_max, s_min, s_max, root=16):
    if v_max <= v_min:
        raise ValueError("v_max must be greater than v_min")
    if root <= 0:
        raise ValueError("root must be greater than zero")

    if abs(s_max - s_min) < 1e-9:
        return f"{s_min / root:.4f}".rstrip("0").rstrip(".") + "rem"

    slope = (s_max - s_min) / (v_max - v_min)
    intercept_rem = (s_min - slope * v_min) / root
    vw = slope * 100
    lower, upper = sorted((s_min, s_max))
    sign = "-" if vw < 0 else "+"

    return (
        f"clamp({lower / root:.4f}rem, "
        f"{intercept_rem:.4f}rem {sign} {abs(vw):.4f}vw, "
        f"{upper / root:.4f}rem)"
    )


print(clamp_from_points(320, 1440, 16, 32))

生产环境应按项目规则统一去掉末尾多余的零。核心是计算过程;格式策略应与其他生成 CSS 保持一致。

用 Bash/awk 在 CI 中校验

一个简单的 CI 校验可能不值得引入语言运行时。下面的 Shell 函数使用 awk 输出 px 版本:

clamp_px() {
  awk -v v1="$1" -v v2="$2" -v s1="$3" -v s2="$4" 'BEGIN {
    if (v2 <= v1) exit 2
    if (s1 == s2) {
      printf "%.4gpx\n", s1
      exit
    }
    m = (s2 - s1) / (v2 - v1)
    b = s1 - m * v1
    lo = s1 < s2 ? s1 : s2
    hi = s1 > s2 ? s1 : s2
    printf "clamp(%.4gpx, %.4gpx %s %.4gvw, %.4gpx)\n", \
      lo, b, m < 0 ? "-" : "+", (m < 0 ? -m : m) * 100, hi
  }'
}

clamp_px 320 1440 16 32

它有意只作为校验助手,而不是完整的 token 编译器。一旦生成流程涉及多种单位、schema 验证或文件输出,经过测试的 JavaScript 或 Python 脚本会更容易维护。

合并前测试 CSS

一份简短清单就能发现公式与布局两类问题:

  • **端点检查:**在最小和最大视口宽度下查看计算值。
  • **中点检查:**确认中心值符合预期插值。
  • **范围外检查:**测试更窄和更宽的视口,确认边界生效。
  • **缩放检查:**使用真实内容,在浏览器 200% 缩放下测试。
  • **本地化检查:**使用支持语言中最长的真实文案,而不是 lorem ipsum。
  • **容器检查:**确认文字可以换行,卡片可以向下增长。
  • **Token 检查:**确认根字体假设与项目一致。
  • **语法检查:**首选表达式中的 +- 两侧必须保留空格。

计算器的曲线与五行采样表覆盖了数值检查。布局检查仍然需要浏览器开发者工具和项目的组件测试完成。

最简单而可靠的规则

使用两个真实端点,推导一条直线,保持上下边界有序,再把结果放进实际组件测试。方法就这么简单。

clamp() 的价值在于消除不必要的断点跳变,而不是把所有 CSS 长度都变成公式。精确的流体值应该比它所替代的媒体查询更容易理解。如果结果需要一整段例外说明,这个布局可能更适合明确的断点。

根据端点生成 CSS clamp() →

延伸阅读

ZeroTool 相关工具