Skip to content

CSS clamp() 应用说明

这篇文章解决什么问题

clamp() 经常被一句话概括成“限制最大最小值”,但真实项目里它解决的不只是限制数值。

它更适合解决这类问题:

  • 字号要跟随屏幕变大,但不能在手机上太小、在大屏上太夸张。
  • 页面左右留白要有呼吸感,但不能把内容挤没。
  • 卡片、图片、侧栏、按钮、圆角、间距都希望自然伸缩,少写断点。
  • 设计稿给了移动端和桌面端两个极值,中间状态不想手写多组媒体查询。
  • 组件需要一套“有边界的弹性尺寸”,既响应式,又不失控。

本文横向介绍 clamp() 的语法、心智模型、和 min() / max() / calc() / media query 的关系,再用真实场景说明它应该放在哪里、怎么写、哪里容易写错。

先说结论

clamp() 的核心价值是:

在最小值和最大值之间,让一个首选值连续变化。

语法是:

css
property: clamp(最小值, 首选值, 最大值);

例如:

css
.title {
  font-size: clamp(1.75rem, calc(1.31rem + 1.88vw), 3rem);
}

意思是:

  • 字号最小不能低于 1.75rem
  • 中间按 calc(1.31rem + 1.88vw) 随视口宽度增长。
  • 字号最大不能超过 3rem

它不是 media query 的替代品。media query 适合做“到某个断点后换布局”,clamp() 适合做“在一个范围内连续缩放”。

clamp() 到底在算什么

可以把它理解成下面这条规则:

css
clamp(min, preferred, max)

正常情况下:

  • preferred < min 时,结果用 min
  • preferred 落在中间时,结果用 preferred
  • preferred > max 时,结果用 max

等价心智模型是:

css
max(min, min(preferred, max))

也就是说,先让值不要超过最大值,再让值不要低于最小值。

如果写成下面这样:

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

minmax 还大,说明表达式本身就有问题。不要依赖这种写法的结果,应该改成清晰的范围。

它和几个相近能力的区别

能力解决什么问题典型写法适合场景
calc()做数学计算calc(100% - 2rem)固定公式、减去边距、比例计算
min()不超过某个上限min(100%, 72rem)容器最大宽度、图片不溢出
max()不低于某个下限max(44px, 2.75rem)可点击区域、最小高度
clamp()在上下限之间连续变化clamp(1rem, 4vw, 3rem)流体字号、间距、图片高度、组件尺寸
media query到断点后切换规则@media (min-width: 768px)布局结构变化、隐藏显示、交互策略变化

一个简单判断:

  • 值只需要“算一下”:用 calc()
  • 值只需要“不超过”:用 min()
  • 值只需要“不低于”:用 max()
  • 值需要“有下限、有弹性、有上限”:用 clamp()
  • 结构要变:用 media query。

最常见场景:流体字号

很多项目会先写成这样:

css
.hero-title {
  font-size: 5vw;
}

问题是:小屏可能太小,大屏可能太大。vw 只知道视口宽度,不知道文本可读性。

更稳妥的写法:

css
.hero-title {
  font-size: clamp(1.75rem, calc(1.31rem + 1.88vw), 3rem);
  line-height: 1.1;
}

这条规则对应一个明确目标:

视口宽度目标字号
375px28px
1440px48px

中间状态用一条直线平滑过渡。视口小于 375px 时,字号不再继续缩小;视口大于 1440px 时,字号不再继续放大。

公式怎么来

如果设计给了两个尺寸点:

项目
小屏宽度375px
小屏字号28px
大屏宽度1440px
大屏字号48px

先算斜率:

text
(48 - 28) / (1440 - 375) * 100 = 1.88vw

再算截距:

text
28 - 375 * 0.0188 = 20.96px

换成 rem 后就是:

css
.hero-title {
  font-size: clamp(1.75rem, calc(1.31rem + 1.88vw), 3rem);
}

这里的 calc(1.31rem + 1.88vw) 比纯 vw 更稳,因为它保留了一部分和根字号相关的尺寸,用户调大默认字号时更不容易被完全绕开。

正文字号不要过度流体化

标题适合明显的流体变化,正文通常不适合大幅伸缩。

推荐:

css
.article {
  font-size: clamp(1rem, calc(0.96rem + 0.2vw), 1.125rem);
  line-height: 1.75;
}

不推荐:

css
.article {
  font-size: clamp(0.875rem, 2vw, 1.5rem);
}

正文的问题不只是大小,还有行长、行高、段落间距。字号变化太大,会让阅读节奏变得不稳定。

间距:让页面呼吸感连续变化

响应式页面经常需要这样的规则:

  • 手机端左右留白小一点。
  • 平板端留白自然变大。
  • 桌面端留白到一个上限后停止。

可以写成:

css
.page-section {
  padding-block: clamp(3rem, 8vw, 7rem);
  padding-inline: clamp(1rem, 4vw, 4rem);
}

这比下面这种断点写法更连续:

css
.page-section {
  padding: 3rem 1rem;
}

@media (min-width: 768px) {
  .page-section {
    padding: 5rem 2rem;
  }
}

@media (min-width: 1200px) {
  .page-section {
    padding: 7rem 4rem;
  }
}

media query 的问题不是不好,而是它会在断点处跳变。间距、字号、圆角这类视觉值通常更适合连续变化。

内容宽度:clamp() 不一定是最佳选择

容器宽度常见写法是:

css
.content {
  width: min(100% - 2rem, 72rem);
  margin-inline: auto;
}

这条规则很清楚:

  • 小屏时左右保留 1rem 空间。
  • 大屏时内容最大 72rem

如果左右留白也想跟随屏幕变化,可以把 clamp() 放进 min() 里:

css
.content {
  width: min(100% - clamp(2rem, 8vw, 8rem), 72rem);
  margin-inline: auto;
}

这里不要强行写成:

css
.content {
  width: clamp(20rem, 86vw, 72rem);
}

原因是:当视口窄到不足 20rem 时,容器可能溢出。内容宽度通常要优先保证“不超过屏幕”,所以 min() 很多时候比单独使用 clamp() 更合适。

网格卡片:控制卡片最小宽度

响应式卡片网格可以这样写:

css
.card-grid {
  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(min(100%, clamp(16rem, 28vw, 22rem)), 1fr)
  );
  gap: clamp(1rem, 2.5vw, 2rem);
}

这条规则做了几件事:

  • 卡片理想宽度会在 16rem22rem 之间变化。
  • 容器太窄时,min(100%, ...) 防止单列卡片撑破容器。
  • auto-fit 负责根据空间自动增加或减少列数。
  • gap 也跟随空间变化,但被限制在合理范围内。

这个例子说明:clamp() 很少孤立使用,它经常和 min()max()minmax()calc() 组合。

图片和首屏高度:避免一屏过满或过空

很多首屏会写:

css
.hero {
  min-height: 100vh;
}

问题是:移动端地址栏、内容长度、CTA 区域都会影响真实观感。首屏不是越满越好。

可以改成:

css
.hero {
  min-height: clamp(32rem, 72svh, 48rem);
  padding-block: clamp(4rem, 10svh, 8rem);
}

.hero-media {
  aspect-ratio: 16 / 10;
  max-height: clamp(18rem, 42vw, 34rem);
  object-fit: cover;
}

这里用 svh 是为了减少移动端浏览器地址栏变化带来的跳动。clamp() 则负责限制高度:小屏不要压得太扁,大屏不要占得太满。

组件尺寸:按钮、头像、图标、圆角

clamp() 不只用于大布局。组件内部也很常用。

css
.user-card {
  padding: clamp(1rem, 2vw, 1.5rem);
  border-radius: clamp(0.75rem, 1.2vw, 1.25rem);
}

.user-card__avatar {
  inline-size: clamp(3rem, 7vw, 4.5rem);
  block-size: clamp(3rem, 7vw, 4.5rem);
}

.user-card__name {
  font-size: clamp(1rem, calc(0.92rem + 0.35vw), 1.25rem);
}

适合放进 clamp() 的组件值:

属性为什么适合
padding小屏节省空间,大屏增加舒适度
gap列表和卡片间距自然变化
border-radius大尺寸组件圆角可以更大,小组件不显得过圆
inline-size / block-size头像、图标、预览图有明确尺寸范围
font-size标题、数字、标签可在范围内缩放
min-height按钮、输入框、卡片保持可点击和可读

但按钮最小高度不要只交给 vw,可点击区域要有稳定下限:

css
.button {
  min-height: max(44px, clamp(2.5rem, 4vw, 3rem));
  padding-inline: clamp(1rem, 2vw, 1.5rem);
}

用 CSS 变量管理一组流体值

如果一个页面里多处使用同一套响应式节奏,可以把它们收敛到页面级变量里。

css
.pricing-page {
  --space-xs: clamp(0.5rem, 1vw, 0.75rem);
  --space-sm: clamp(0.75rem, 1.5vw, 1rem);
  --space-md: clamp(1rem, 2vw, 1.5rem);
  --space-lg: clamp(1.5rem, 4vw, 3rem);
  --space-xl: clamp(3rem, 8vw, 6rem);

  --title-size: clamp(2rem, calc(1.35rem + 2.8vw), 4rem);
  --card-radius: clamp(0.75rem, 1.2vw, 1.25rem);
}

.pricing-page__hero {
  padding-block: var(--space-xl);
}

.pricing-card {
  padding: var(--space-lg);
  border-radius: var(--card-radius);
}

.pricing-card h2 {
  font-size: var(--title-size);
}

这样做的好处不是“抽象”,而是让同一页的尺寸节奏一致。文章页、定价页、仪表盘、活动页的节奏通常不同,变量也应该放在页面或模块内,不一定要做成全站公共规则。

和 container query 的关系

vw 参考的是视口宽度,不是组件所在容器宽度。

如果一个卡片会出现在主内容区、侧栏、弹窗里,单纯用 vw 可能不准确。因为视口很宽时,侧栏里的卡片并没有那么宽。

有两种处理方式。

方式一:用容器查询切换关键结构

css
.profile-panel {
  container-type: inline-size;
}

.profile-card {
  display: grid;
  gap: clamp(0.75rem, 2vw, 1.25rem);
}

@container (min-width: 36rem) {
  .profile-card {
    grid-template-columns: auto 1fr;
    align-items: center;
  }
}

这里 clamp() 负责连续变化,container query 负责结构变化。

方式二:用容器单位

如果项目目标浏览器允许使用容器单位,可以把 vw 换成 cqw

css
.profile-panel {
  container-type: inline-size;
}

.profile-card__title {
  font-size: clamp(1.125rem, calc(0.9rem + 2cqw), 1.5rem);
}

cqw 参考容器宽度,适合组件内的流体尺寸。项目对兼容性要求严格时,要先确认目标浏览器支持情况。

clamp() 适合什么,不适合什么

场景是否适合原因
标题字号适合有明确最小和最大视觉范围
正文字号谨慎变化幅度要很小,否则影响阅读
页面间距适合连续变化比断点跳变自然
容器最大宽度不一定很多时候 min() 更清楚
卡片网格适合组合使用常和 minmax()auto-fit 一起用
复杂布局切换不适合单独使用结构变化仍应使用 media query 或 container query
隐藏显示不适合clamp() 是算值,不是条件渲染
颜色、主题不适合它解决的是数值范围,不是状态选择

常见错误

1. 把 clamp() 当断点

不推荐:

css
.sidebar {
  width: clamp(0px, 50vw, 320px);
}

如果侧栏在手机端应该隐藏,在桌面端应该显示,这属于结构或状态问题,应该用 media query:

css
.sidebar {
  display: none;
}

@media (min-width: 960px) {
  .sidebar {
    display: block;
    width: 320px;
  }
}

2. 首选值太激进

不推荐:

css
.title {
  font-size: clamp(1.5rem, 10vw, 5rem);
}

10vw 会让中间区间变化过猛。更稳妥:

css
.title {
  font-size: clamp(1.5rem, calc(1rem + 2.5vw), 4rem);
}

3. 只用 vw 做文字

不推荐:

css
.title {
  font-size: clamp(1.5rem, 6vw, 4rem);
}

更推荐:

css
.title {
  font-size: clamp(1.5rem, calc(1rem + 2.5vw), 4rem);
}

混入 rem 后,字号表达式不会完全依赖视口宽度,对用户字号设置更友好。

4. 忘记内容本身的限制

clamp() 只能限制数值,不能保证内容一定放得下。

css
.tag {
  inline-size: clamp(6rem, 20vw, 12rem);
}

如果标签文本很长,仍然要处理换行、截断或布局:

css
.tag {
  inline-size: clamp(6rem, 20vw, 12rem);
  overflow-wrap: anywhere;
}

实战选择流程

clamp() 前,可以按下面顺序判断:

  1. 这个值有没有明确的最小值?
  2. 这个值有没有明确的最大值?
  3. 中间是否应该连续变化,而不是断点跳变?
  4. 变化依据是视口、容器、还是父元素比例?
  5. 如果内容变长、系统字号变大、屏幕变窄,会不会溢出?

只要第 1、2、3 条都成立,clamp() 通常就是合适选择。

一套可直接参考的页面写法

css
.marketing-page {
  --page-gutter: clamp(1rem, 4vw, 4rem);
  --section-space: clamp(4rem, 9vw, 8rem);
  --card-gap: clamp(1rem, 2vw, 1.75rem);
  --hero-title: clamp(2.25rem, calc(1.4rem + 3.6vw), 5rem);
  --section-title: clamp(1.75rem, calc(1.25rem + 2vw), 3rem);
  --body-size: clamp(1rem, calc(0.96rem + 0.2vw), 1.125rem);
}

.marketing-page__section {
  padding-block: var(--section-space);
  padding-inline: var(--page-gutter);
}

.marketing-page__inner {
  width: min(100%, 72rem);
  margin-inline: auto;
}

.marketing-page__hero-title {
  max-width: 12ch;
  font-size: var(--hero-title);
  line-height: 1.05;
}

.marketing-page__lead {
  max-width: 62ch;
  font-size: var(--body-size);
  line-height: 1.75;
}

.marketing-page__grid {
  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(min(100%, clamp(16rem, 28vw, 22rem)), 1fr)
  );
  gap: var(--card-gap);
}

.marketing-page__card {
  padding: clamp(1rem, 2vw, 1.5rem);
  border-radius: clamp(0.75rem, 1vw, 1.25rem);
}

这套写法的重点是:

  • 页面留白、区块间距、标题、正文、网格、卡片都各自有边界。
  • 容器宽度用 min(),避免小屏溢出。
  • 网格列宽用 minmax(),让结构自己适配。
  • clamp() 只负责那些确实需要连续变化的视觉值。

总结

clamp() 最重要的不是语法,而是边界意识。

它适合把设计稿里的两个端点变成一条连续的响应式曲线:小屏不再被压得过小,大屏不再无限放大,中间状态不需要一堆断点硬切。

真正好用的方式不是到处写 clamp(),而是把它放在合适的位置:

  • 字号、间距、圆角、图片高度、组件尺寸,用它很合适。
  • 容器宽度、结构切换、隐藏显示,不要硬套。
  • min()max()calc()minmax()、media query、container query 组合使用,才是它在真实项目里的常见形态。

参考资料

基于 VitePress 的个人知识库骨架