过去曾尝试过使用多种方法(例如媒体查询和 CSS calc()
)来实现响应式排版。
在这里,我们将探索一种不同的方法,用于随着视口宽度的增加在最小和最大尺寸之间线性缩放文本,旨在使其在不同屏幕尺寸下的行为更可预测——这一切都只需一行 CSS 代码,这要归功于 clamp()
。
CSS 函数 clamp()
是一款强大的工具。它可用于各种用途,但在排版方面尤其出色。以下是它的工作原理。它接受三个值:
clamp(minimum, preferred, maximum);
它返回的值将是首选值,直到该首选值低于最小值(此时将返回最小值)或高于最大值(此时将返回最大值)。
那么,假设您没有设置奇怪的值并将其设置为最小值和最大值之间,它是否总是首选值呢?好吧,您很可能会为首选值使用公式,例如
.banner {
width: clamp(200px, 50% + 20px, 800px); /* Yes, you can do math inside clamp()! */
}
假设您希望在视口宽度为 360px 或以下时将元素的最小 font-size
设置为 1rem,并在视口宽度为 840px 或以上时将其最大值设置为 3.5rem。
换句话说
1rem = 360px and below
Scaled = 361px - 839px
3.5rem = 840px and above
任何介于 361 和 839 像素之间的视口宽度都需要一个在 1 和 3.5rem 之间线性缩放的字体大小。使用 clamp()
这实际上非常容易!例如,在视口宽度为 600 像素时(介于 360 和 840 像素之间的一半),我们将获得 1 和 3.5rem 之间的确切中间值,即 2.25rem。

我们试图使用 clamp()
实现的目标称为线性插值:获取两个数据点之间的中间信息。
以下是执行此操作的四个步骤
步骤 1
选择您的最小和最大字体大小,以及您的最小和最大视口宽度。在我们的示例中,字体大小为 1rem 和 3.5rem,宽度为 360px 和 840px。
步骤 2
将宽度转换为 rem
。由于大多数浏览器中 1rem 默认值为 16px(稍后详细介绍),因此我们将使用它。因此,现在最小和最大视口宽度将分别为 22.5rem 和 52.5rem。
步骤 3
在这里,我们将稍微偏向数学方面。当视口宽度和字体大小配对时,它们在 X 和 Y 坐标系上形成了两个点,而这些点构成了一条线。

(22.5, 1)
和 (52.5, 3.5)
我们需要这条线——或者更准确地说,我们需要它的斜率及其与 Y 轴的交点。以下是计算方法
slope = (maxFontSize - minFontSize) / (maxWidth - minWidth)
yAxisIntersection = -minWidth * slope + minFontSize
这给了我们 0.0833 的斜率值和 -0.875 的 Y 轴交点值。
步骤 4
现在我们构建 clamp()
函数。首选值的公式为
preferredValue = yAxisIntersection[rem] + (slope * 100)[vw]
因此函数最终如下所示
.header {
font-size: clamp(1rem, -0.875rem + 8.333vw, 3.5rem);
}
您可以在以下演示中可视化结果
继续操作并尝试一下。如您所见,当视口宽度为 840px 时,字体大小停止增长,并在 360px 时停止缩小。介于两者之间的所有内容都以线性方式变化。
如果用户更改根字体大小会怎样?
您可能已经注意到这种方法的一个小缺陷:它只有在根字体大小与您认为的一致(在前面的示例中为 16px)且从不更改时才有效。
我们将宽度 360px 和 840px 转换为 rem
单位,方法是将它们除以 16,因为我们假设这是根字体大小。如果用户将其首选项设置为其他根字体大小(例如 18px 而不是默认的 16px),则该计算将出错,并且文本不会以我们期望的方式调整大小。
这里只有一种方法可以使用,那就是 (1) 在页面加载时在代码中进行必要的计算,(2) 侦听根字体大小的变化,以及 (3) 如果发生任何更改则重新计算所有内容。
以下是一个用于执行计算的有用 JavaScript 函数
// Takes the viewport widths in pixels and the font sizes in rem
function clampBuilder( minWidthPx, maxWidthPx, minFontSize, maxFontSize ) {
const root = document.querySelector( "html" );
const pixelsPerRem = Number( getComputedStyle( root ).fontSize.slice( 0,-2 ) );
const minWidth = minWidthPx / pixelsPerRem;
const maxWidth = maxWidthPx / pixelsPerRem;
const slope = ( maxFontSize - minFontSize ) / ( maxWidth - minWidth );
const yAxisIntersection = -minWidth * slope + minFontSize
return `clamp( ${ minFontSize }rem, ${ yAxisIntersection }rem + ${ slope * 100 }vw, ${ maxFontSize }rem )`;
}
// clampBuilder( 360, 840, 1, 3.5 ) -> "clamp( 1rem, -0.875rem + 8.333vw, 3.5rem )"
我故意省略了如何将返回的字符串注入 CSS,因为根据您的需求以及您是否使用原生 CSS、CSS-in-JS 库或其他内容,有许多方法可以做到这一点。此外,没有针对字体大小更改的原生事件,因此我们必须手动检查。我们可以使用 setInterval
每秒检查一次,但这可能会带来性能损失。
这更多的是一个极端情况。很少有人会更改其浏览器的字体大小,更少的人会在访问您的网站时更改它。但是,如果您希望您的网站尽可能地具有响应性,那么这就是方法。
对于那些不介意这种极端情况的人
更新:自从本文首次发布以来,此处共享的资源已停止工作。如果您正在寻找计算器来帮助确定各种视口下的字体大小,请考虑使用 流体字体生成器,它使用了现代的流体排版技术。
如何避免文本重排
对排版尺寸进行如此细粒度的控制使我们能够做其他一些很酷的事情——例如阻止文本在不同的视口宽度下重排。
这是文本的正常行为。


但是现在,凭借我们所拥有的控制力,我们可以使文本保持相同的行数,始终在同一个单词处断开,无论我们使用什么视口宽度。


那么我们如何做到这一点呢?首先,字体大小和视口宽度之间的比率必须保持不变。在此示例中,我们从 320px 处的 1rem 更改到 960px 处的 3rem。
320 / 1 = 320
960 / 3 = 320
如果我们使用之前创建的 clampBuilder()
函数,则会变成
const text = document.querySelector( "p" );
text.style.fontSize = clampBuilder( 320, 960, 1, 3 );
它保持相同的宽度与字体比率。我们这样做的原因是,我们需要确保文本在每个宽度下都具有正确的尺寸,以便能够保持相同的行数。它仍然会在不同的宽度下重排,但这样做对于我们接下来要做的操作是必要的。
现在我们必须借助 CSS 字符 (ch
) 单位,因为仅使字体大小正确是不够的。一个 ch
单位相当于元素字体中“0”字形的宽度。我们希望使文本主体与视口一样宽,不是通过设置 width: 100%
,而是使用 width: Xch
,其中 X
是水平填充视口所需的 ch
单位(或 0)数量。
为了找到X
,我们必须将最小视口宽度 320px 除以元素在视口宽度为 320px 时的 ch
大小。在本例中,字体大小为 1rem。
别担心,这里有一段代码可以计算元素的 ch
大小
// Returns the width, in pixels, of the "0" glyph of an element at a desired font size
function calculateCh( element, fontSize ) {
const zero = document.createElement( "span" );
zero.innerText = "0";
zero.style.position = "absolute";
zero.style.fontSize = fontSize;
element.appendChild( zero );
const chPixels = zero.getBoundingClientRect().width;
element.removeChild( zero );
return chPixels;
}
现在我们可以继续设置文本的宽度了
function calculateCh( element, fontSize ) { ... }
const text = document.querySelector( "p" );
text.style.fontSize = clampBuilder( 320, 960, 1, 3 );
text.style.width = `${ 320 / calculateCh(text, "1rem" ) }ch`;

哇,等等。发生了一些不好的事情。有一个水平滚动条搞砸了事情!
当我们谈论 320px 时,我们指的是视口的宽度,包括垂直滚动条。因此,文本的宽度被设置为可见区域的宽度加上滚动条的宽度,这会导致它水平溢出。
那么为什么不使用一个不包含垂直滚动条宽度的度量呢?我们做不到,这是因为 CSS 的 vw
单位。请记住,我们在 clamp()
中使用 vw
来控制字体大小。你看,vw
包括垂直滚动条的宽度,这使得字体随着包括滚动条在内的视口宽度一起缩放。如果我们想避免任何重排,那么宽度必须与视口的任何宽度成比例,包括滚动条。
那么我们该怎么做呢?当我们这样做时
text.style.width = `${ 320 / calculateCh(text, "1rem") }ch`;
…我们可以通过将其乘以小于 1 的数字来缩小结果。0.9 可以解决问题。这意味着文本的宽度将是视口宽度的 90%,这将足以解决滚动条占用的一小部分空间。我们可以通过使用更小的数字(例如 0.6)来使其更窄。
function calculateCh( element, fontSize ) { ... }
const text = document.querySelector( "p" );
text.style.fontSize = clampBuilder( 20, 960, 1, 3 );
text.style.width = `${ 320 / calculateCh(text, "1rem" ) * 0.9 }ch`;

您可能很想简单地从 320 中减去几个像素以忽略滚动条,如下所示
text.style.width = `${ ( 320 - 30 ) / calculateCh( text, "1rem" ) }ch`;
这样做的的问题是它会带来重排问题!这是因为从 320 中减去会破坏视口与字体的比例。


文本的宽度必须始终是视口宽度的百分比。另一件需要注意的事情是我们需要确保我们在每个使用该网站的设备上都加载相同的字体。这听起来很明显,不是吗?好吧,这里有一个可能导致文本错位的细节。执行诸如 font-family: sans-serif
之类的操作不能保证在每个浏览器中都使用相同的字体。sans-serif
将在 Windows 版 Chrome 上设置 Arial,但在 Android 版 Chrome 上设置 Roboto。此外,某些字体的几何形状可能会导致重排,即使您做对了所有事情。等宽字体往往会产生最佳效果。因此,始终确保您的字体准确无误。
在以下演示中查看此无重排示例
容器内的无重排文本
我们现在要做的就是在容器上应用字体大小和宽度,而不是直接在文本元素上应用。其中的文本只需要设置为 width: 100%
。对于段落和标题,这不是必需的,因为它们无论如何都是块级元素,并且会自动填充容器的宽度。
在父容器中应用此方法的一个优点是,其子元素将自动响应和调整大小,而无需逐个设置其字体大小和宽度。此外,如果我们需要更改单个元素的字体大小而不影响其他元素,我们只需将其字体大小更改为任何 em
值,它将自然地相对于容器的字体大小。
无重排文本很挑剔,但它是一种微妙的效果,可以为设计带来一丝优雅!
总结
最后,我整理了一个小演示,展示了所有这些内容在现实场景中的外观。
在这个最终示例中,您还可以更改根字体大小,并且 clamp()
函数将自动重新计算,以便文本在任何情况下都具有正确的大小。
尽管本文的目标是将 clamp()
与字体大小一起使用,但相同的技术可用于接收长度单位的任何 CSS 属性。现在,我并不是说你应该在任何地方都使用它。很多时候,一个简单的 font-size: 1rem
就足够了。我只是想向您展示在需要时您可以拥有多少控制权。
就我个人而言,我认为 clamp()
是 CSS 中出现的最佳功能之一,我迫不及待地想看看随着它的普及,人们会想出哪些其他用法!
我没想到
clamp
允许删除媒体查询以使用线性插值计算font-size
,谢谢!我认为您的clampBuilder()
可以用纯 CSS 替换我一直将此用于可缩放字体
font-size: calc(14px + (36 - 14) * ((100vw - 320px) / (2560 - 320));
与这种方法相比,clamp 的优势是什么——仅仅是更少的语法和可能避免媒体查询?哪种支持最好?谢谢 :)
嗨,Kev,我不确定你是否会看到这条评论,但本着为任何人写一些有趣的东西的精神……
多年来我也一直在使用您的公式(尽管使用 rem 单位而不是 px),这要感谢Mike Riethmuller)。我后悔这篇文章没有提到他的方法并与“clamp 方法”进行比较。我也认为唯一的区别是语法更短。
calc()
受 IE 支持,而clamp()
不支持。calc() 技巧太酷了!唯一的缺点是它不会在最小和最大限制处停止,而 clamp 会停止。clamp 的缺点是目前支持度较低。
所以我现在实际上同时使用这两种方法,我发现它很方便。它看起来像这样
font-size:calc(16px + (28 - 16) * ((100vw - 360px) / 1560));
font-size: clamp(16px, calc(16px + (28 - 16) * ((100vw - 360px) / 1560)), 28px);
我正在开发一个 CMS,用户可以在其中选择最小和最大字体大小,因此我可以在现成的模板式代码中捕获这些公式,从而节省大量时间,而无需使用 JavaScript。
非常酷的概念。但在 macOS/Safari (13.1.2) 上仅在第一次渲染时有效——如果调整视口大小,字体大小不会动态更改。我敢打赌,如果您旋转设备,iOS/Webkit 上的情况也是如此。所以(遗憾的是)尚未准备好投入生产。但无论如何都很酷!感谢您的辛勤工作。
Safari 从 13.7 开始才支持 clamp()。您可以使用 MDN 上提到的嵌套 min()/max() 技巧
clamp(MIN, VAL, MAX) 解析为 max(MIN, min(VAL, MAX))
https://mdn.org.cn/en-US/docs/Web/CSS/clamp
如果为所有具有动态字体大小的元素设置
min-height: 0vh
,则字体缩放可在 Safari 中工作。请谨慎使用最大文本大小,尤其是在面向公众或员工的网站/页面上。如果您阻止文本放大 200%,那么这将是WCAG SC 1.4.4 的 AA 级失败。视口单位在其自己的说明中被列为 WCAG 的主要风险。
无论您使用什么技术,请确保页面文本至少可以放大 200%。不出所料,我写过关于响应式字体和缩放的文章,并对
min()
、max()
和clamp()
提出过警告。我理解这些示例仅用于演示目的,但没有一个允许我将文本缩放超过 175%。如果将示例代码按原样复制到项目中,该项目可能会无法通过可访问性审核。
嘿,Adrian,感谢分享!我之前并不知道。但是,我不明白为什么您无法缩放超过 175%。可能是因为我的测试方法不正确。我该如何检查?我在 Firefox 中启用了“仅缩放文本”并将缩放比例设置为 200%,并且效果很好。我可以理解对于“vh”来说这是一个问题,但对于“vw”(如上例所示)来说,这如何成为障碍?谢谢
Bregt,打开您的笔的调试模式,例如cdpn.io/pprg1996/debug/yLONLPv,然后使用本机浏览器缩放功能——在 Windows 上是
Ctrl
+ 鼠标滚轮向上或Ctrl
++
(是的,就是加号键)。在缩放时,请注意地址栏中的缩放级别。您应该注意到文本在某个时刻停止变大。
为了更直观地说明这一点,请尝试将视口宽度设置为 800px。现在尝试缩放。观察当您尝试缩放时,文本是如何变小的。
如果需要,我可以为您制作视频或截图。
Bregt,我制作了一个视频来展示问题以及一些截图。
https://adrianroselli.com/2019/12/responsive-type-and-zoom.html#Update04
现在尝试使用 800px 视口。您会发现缩放页面会使文本缩小。
谢谢 Adrian。我也注意到了,但在宽屏上没有这个问题。尝试使用 clamp() 和 max()/min() 的好方法是什么?
我在我的函数中使用比 vw 加法更大的 rem(例如,max(2.1875rem, min(1.9375rem + 1.1111111111vw, 2.8125rem),我使用 max(min()) 而不是 clamp() 来支持旧版 Safari 浏览器)。
Beegt,为了回答你的问题“尝试使用 clamp() 和 max()/min() 的好方法是什么?”,我建议完全不要使用
clamp()
甚至max()
——至少在没有测试并确认使用可以在任何窗口大小下将文本缩放 200% 之前。至少在 Chrome 85 中的 codepen“全页面视图”中,可以放大到 500%,文本大小一直增大到最后.. 从外观上看,它超过了原始文本大小的 175%… 或许他们修复了这个问题?
那么这种技术可以用于字体大小之外的其他属性吗?
行高、填充、边距、边框宽度等也可以灵活吗?
我已经使用 Indrix Pass 的 mixin 一段时间了,效果很好,但我完全赞成更原生方法:http://sassmeister.com/gist/7f22e44ace49b5124eec
几乎可以在任何可以使用单位的地方使用,所以是的!
这项技术看起来很酷,但我可以问一个简单的问题吗?(或几个)
为什么我们想要在更大的屏幕上将文本缩放得更大?我的意思是,为什么要一开始就经历所有这些麻烦?我的意思是,我们是否假设用户离屏幕的距离是近还是远?通过剥夺用户已经拥有的常规 UI 和文本缩放选项的控制权,用户是否会获得一些我在这里看不到的意想不到的好处?
不幸的是,使用 clamp() 进行流体排版会阻止用户更改浏览器中的默认字体大小。
它不会。缩放是相对于根字体大小的。
嗯…在 Chrome 中测试过。更改浏览器设置中的默认字体大小对节点没有影响,这些节点的字体大小由 clamp 函数定义。我说的不是根字体大小。我指的是根字体大小未明确指定且用户可以自行在设置中更改它的情况。
那是因为 Chrome 需要重新加载才能将更改应用于浏览器的字体大小。用 Firefox 再次尝试,它会即时更改。
这不是 clamp 的错,这只是 Chrome 实现其字体大小更改功能的方式。
有人可以用这些函数在 scss 中创建 mixin 吗?
我也尝试找到一个完全执行此操作的混合,但我找不到任何东西 =(
这是一个 Sass 函数。输入大小以像素为单位(因为我觉得它更直观)。
@function betterClamp($minSize, $maxSize, $minWidth: 480, $maxWidth: 1536) {
// 转换为 rem
$minSize: $minSize / 16;
$maxSize: $maxSize / 16;
$maxWidth : $maxWidth / 16;
$minWidth : $minWidth / 16;
// 进行计算
$slope: ($maxSize - $minSize) / ($maxWidth - $minWidth);
$yAxisIntersection: -$minWidth * $slope + $minSize;
$preferredValue: #{$yAxisIntersection * 1rem} + #{$slope * 100vw};
// 输出为 rem
$minSize: $minSize * 1rem;
$maxSize: $maxSize * 1rem;
@return clamp($minSize, $preferredValue, $maxSize);
}
更新以使用更新的语法
https://pastebin.com/Ej3Hi4Lx
哦,不错,这是一个绝对的改变游戏规则的东西。非常感谢您抽出时间分享这一点:) 我试图弄清楚如何在 Django 网站上创建响应式富文本字段——这非常完美。
你好!如果我们不知道最小和最大视口宽度怎么办?我的意思是,也许一个用例只是一个全屏网站,其中我有一个包含文本的全宽元素。