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);min 比 max 还大,说明表达式本身就有问题。不要依赖这种写法的结果,应该改成清晰的范围。
它和几个相近能力的区别
| 能力 | 解决什么问题 | 典型写法 | 适合场景 |
|---|---|---|---|
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;
}这条规则对应一个明确目标:
| 视口宽度 | 目标字号 |
|---|---|
| 375px | 28px |
| 1440px | 48px |
中间状态用一条直线平滑过渡。视口小于 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);
}这条规则做了几件事:
- 卡片理想宽度会在
16rem到22rem之间变化。 - 容器太窄时,
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 条都成立,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 组合使用,才是它在真实项目里的常见形态。