现代 CSS

聊聊min(),max()和clamp()函数

特别声明:如果您喜欢小站的内容,可以点击申请会员进行全站阅读。如果您对付费阅读有任何建议或想法,欢迎发送邮件至: airenliao@gmail.com!或添加QQ:874472854(^_^)

CSS函数中的min()max()clamp()CSS值和单位模块第九部分,它们常被称为比较函数。这几个函数并不是什么最新的特性,早在2018年年底就有浏览器开始支持它们,今年4月份Firefox也开始支持这几个函数,这也意味着现在所有主流浏览器都支持这些函数。它们最大的作用就是可以为我们提供动态布局和更灵活设计组件方法。简单地说,这几个函数可以用来设置元素尺寸,比如容器大小,字号大小,内距,外距等。在这篇文章中,我将用一些示例和大家一起来探讨这几个函数在实际中的使用,希望能更好的帮助大家理解它们。

兼容性

Caniuse.com中的数据可以得知,到写这篇文章为止,min()max()clamp()函数已经得所有主流浏览器的支持了。

虽然说这几个函数得到主流浏览器的支持,但要用于实际项目中时,还是需要谨慎,或者说要做好相应的降级处理。比如人肉做降级处理:

.element {
    font-size: 20px;
    font-size: clamp(20px, (1rem + 3vw), 40px)
}

也可以通过@supports来做条件判断

.element {
    font-size: 20px;
}

@supports (width: min(10px, 3vw)) {
    .element {
        font-size: clamp(20px, (1rem + 3vw), 40px)
    }
}

不过这并不是阻碍我们继续往下阅读的主要原因。

在此之前

熟悉CSS的同学都知道可以使用min-width/heightmax-width/heightmin-contentmax-content等属性来设置容器尺寸,但这些属性并无法用来指定非容器的尺寸,比如字号大小,就没有min-font-sizemax-font-size属性。但在CSS的Grid布局中,有另一个函数minmax()更接近我们今天要聊的min()max()函数。不过,它们还是有着本质上的区别,当你阅读完后面的内容你就会更清楚。

共同的特征

如果你曾使用过minmax()函数的话,该函数能接受两个值,其中之一是最小值,另一个则是最大值。而min()max()clamp()函数可以用来比较多个值,并基于所使用的函数,其中一个值会被运用到CSS的属性上(当作属性值)。比如:

width: min(1vw, 4em, 80px)
width: max(1vw, 4em, 80px)
width: clamp(1vw, 4em, 80px)

还可以有更多的值,只需要注意,每个值之间必须使用逗号(,)来分隔:

property: min(value [, value]) || max(value [, value]) || clamp(value [, value])

而且,它们都可以像calc()数学函数,可以使用加、减、乘、除等四则运算的表达式,比如:

font-size: max(10 * (1vw + 1vh) / 2, 12px);
font-size: clamp(12px, 10 * (1vw + 1vh) / 2, 100px);

即:

property: min(expression [, expression]) || max(expression [, expression]) || clamp(expression [, expression])

另外,它们都可以用于以下任何属性中<length><frequency><angle><time><percentage><number>或者<integer>

还有就是,它们之间可以相互嵌套,而且还可以和calc()相互嵌套使用:

width: max(200px, min(50%, 1000px));
width: calc(min(800px, 100vw) / 6);

你可能还不知道上面示例代码所表达的含义,但不用着急,随着你继续往下阅读,你会明白它们的含义和所起作用。

语法

min()max()clamp()函数有点类似于calc()函数,也称为数学函数(Math Function),允许使用带有加法(+)减法(-)乘法(*)除法(/)的数学表达式作为分量值(Component Values)min()max()函数分别表示其包含的最小或最大的逗号分隔计算;clamp()函数表示其中心计算,返回一个区间范围的值(MIN,VAL,MAX),如果值(VAL)在最小(MIN)和最大值(MAX)区间内,则使用该值(VAL),如果值(VAL)大于最大值(MAX),则使用最大值(MAX),如果值(VAL)小于最小值(MIN),则使用最小值(MIN)。

它们的具体语法规则如下:

<min()> = min(<calc-sum>#)
<max()> = max(<calc-sum>#)
<clamp()> = clamp(<calc-sum>#3{})

其中<calc-sum>

<calc-sum> = <calc-product> [ [ '+' | '-' ] <calc-product> ]*
<calc-product> = <calc-value> [[ '*' | '/'] <calc-value>]*
<calc-value> = <number> | <dimension> | <percentage> | (<calc-sum>)

此外,+-操作符的两边都需要有空格,*/操作符可以在没有空格的情况下使用。

是不是看到上面的表达式人就晕了,如果是的话,请继续往下阅读,我们将用更简单的方式来阐述min()max()clamp()函数。

min()函数

将其语法简单化:

min(expression [, expression])

即,min()函数包含一个或多个逗号分隔的计算(表达式),然后以最小的表达式的值作为返回值。

使用min()设置最大值

看到这样的描述,你可能会感到困惑,min()表示最小,怎么又是用来设置最大值呢?这不相互矛盾了?其实不是的,我们来看一个示例:

.element {
    width: min(50vw, 500px);
}

比如浏览器视窗现在所处的位置是1200px的宽度,那么.element渲染的结果如下:

这个时候.element元素的width500px。此时,min(50vw, 500px)相当于是min(600px, 500px),也有点类似于.element设置了max-width: 500px。也就是大家最终看到的效果。

如果我们把浏览器视窗缩小至760px

这个时候.element元素的width50vw(就上图对应的380px)。这个时候min(50vw, 500px)有点类似于设置了width: 50vw(或者说min-width: 50vw)。

也就是说,就该示例而言,.element选择min(50vw, 500px)值,取决于浏览器视窗宽度。如果50vw计算的值大于500px,那么50vw将会被忽略,元素.element宽度取值为500px;反之,如果50vw计算的值小于500px,那么则将50vw作为.element的宽度值。

尝试着拖动浏览器视窗的大小,你可以看到类似下图这样的效果:

正如上面示例所示,min()函数最终返回值和函数中设置的值单位有着直接关系,比如上面示例中的50vw是和视窗单位的计算有关,如果换成下面这样的示例:

.element {
    width: min(30%, 50em, 500px)
}

那么%是和父容器宽度有关,em和元素自身font-size有关。

如果你还不能很好的理解min()函数的话,那么可以和我们以往熟悉的案例结合起来。比如我们构建一个响应式布局,在PC端希望容器的宽度是1024px,而在移动端(比如手机端)宽度的宽度是100%。以往可能会这样写我们的CSS:

.container {
    width: 1024px;
    max-width: 100%;
}

要是用min()函数来表达的话,就可以像下面这样:

.container {
    width: min(1024px, 100%)
}

min()函数也可以接受数学表达式:

.element {
    width: min(calc(50px * 10), 100rem);
    border-width: min(5px, var(--borderWidthh));
    padding: min(5px * 2, 2em);
}

max()函数

max()函数和min()函数刚好相反,返回的是最大值。使用max()设置一个最小值。我们把上面的示例中的min()函数换成max()函数:

.element {
    width: max(50vw, 500px)
}

假设浏览器视窗现在宽度是1200px,那么.element渲染的结果如下:

这个时候,.element元素的width600px(对应的50vw,即1200px / 2 = 600px)。此时max(50vw, 500px)相当于是max(600px, 500px),也有点类似于.element设置了width: 600px。也就是大家最终看到的效果。

如果我们把浏览器视窗缩小至760px

这个时候.element元素的width500px。这个时候max(50vw, 500px)有点类似于设置了min-width: 500px

也就是说,就该示例而言,.element选择max(50vw, 500px)值,取决于浏览器视窗宽度。如果50vw计算的值小于500px,那么50vw将会被忽略,元素.element的宽度取值为500px;反之,如果50vw计算的值大于500px,那么则将50vw作为.element的宽度值。

尝试着拖动浏览器视窗的大小,你可以看到类似下图这样的效果:

clamp()函数

clamp()函数和min()以及max()不同,它返回的是一个区间值。clamp()函数接受三个参数,即 clamp(MIN, VAL, MAX),其中MIN表示最小值,VAL表示首选值,MAX表示最大值。它们之间:

  • 如果VALMINMAX之间,则使用VAL作为函数的返回值;
  • 如果VAL大于MAX,则使用MAX作为函数的返回值;
  • 如果VAL小于MIN,则使用MIN作为函数的返回值

我们来看一个示例:

.element {
    /**
    * MIN = 100px
    * VAL = 50vw ➜ 根据视窗的宽度计算
    * MAX = 500px
    **/
    width: clamp(100px, 50vw, 500px);
}

比如浏览器视窗现在所处的位置是1200px的宽度,那么.element渲染的结果如下:

这个时候.element元素的width500px。此时,clamp(100px, 50vw, 500px),相当于clamp(100px, 600px, 500px),对应的VAL值是600px,大于MAX值,那么这个时候clamp()函数返回的值是MAX,即500px,这个时候.elementwidth值就是500px(即MAX的值)。

如果我们把浏览器视窗缩小至760px

这个时候.element元素的width50vw。此时,clamp(100px, 50vw, 500px),相当于clamp(100px, 380px, 500px),对应的VAL值是380px,该值大于MIN值(100px),小于MAX值(500px),那么这个时候clamp()函数返回的值是VAL,即50vw,这个时候.elementwidth值就是50vw(即VAL的值)。

如果继续将浏览器的视窗缩小至170px:

这个时候.element元素的width100px。此时,clamp(100px, 50vw, 500px),相当于clamp(100px, 85px, 500px),对应的VAL值是85px,该值小于MIN值(100px),那么这个时候clamp()函数返回的值是MIN,即100px,这个时候.elementwidth值就是100px(即MIN的值)。

就该示例而言,clamp(100px, 50vw, 500px)还可以这样来理解:

  • 元素.element的宽度不会小于100px(有点类似于元素设置了min-width: 100px
  • 元素.element的宽度不会大于500px(有点类似于元素设置了max-width: 500px
  • 首选值VAL50vw,只有当视窗的宽度大于200px且小于1000px时才会有效,即元素.element的宽度为50vw(有点类似于元素设置了width: 50vw

具体效果如下:

尝试着拖动浏览器视窗的大小,你可以看到类似下图这样的效果:

如果使用了clamp()函数的话,相当于使用了min()max()函数,具体地说:

clamp(MIN, VAL, MAX) = max(MIN, min(VAL, MAX))

就上面示例而言:

.element {
    width: clamp(100px, 50vw, 500px)
}

和下面的代码等效:

.element {
    width: max(100px, max(50vw, 500px))
}

正如前面所述,clamp()的计算会经历以下几个步骤:

.element {
    width: clamp(100px, 50vw, 500px);

    /* 50vw相当于视窗宽度的一半,如果视窗宽度是760px的话,那么50vw相当等于380px*/
    width: clamp(100px, 380px, 500px);

    /* 用min()和max()描述*/
    width: max(100px, min(380px, 500px))

    /*min(380px, 500px)返回的值是380px*/
    width: max(100px, 380px)

    /*max(100px, 380px)返回的值是380px*/
    width: 380px;
}

这和前面介绍clamp(MIN,VAL,MAX)时计算出来结果一致(VAL大于MIN且小于MAX会取首选值VAL)。

注意事项

不知道你有没有发现,在前面的示例中,我们在min()max()clamp()函数参数使用vw时,最终的计算值是依据视窗的宽度来计算的。换句话说,我们在使用这些函数时,参数中运用的不同值单位对最终值是有一定的影响,也就是说:

min()max()clamp()函数中的参数,计算的值取决于上下文

如果你阅读《CSS 的值和单位》一文,你会发现,在CSS中很多取值的单位是和上下文有关系的,比如我们熟悉的vwvh%remem等。就拿%单位来说吧,如果元素是<body>子元素,那么它的计算方式就是基于浏览器视窗宽度来计算,如果元素是另一个元素的子元素,那这个时候可能会基于它的父元素来计算。再比如说,rem单位,它始终会基于<html>元素的font-size来计算。

可以说,这些相对单位对于min()max()clamp()返回值有着直接的影响。比如下面这个示例来说吧:

.element {
    width: min(10vw, 10rem, 100px)
}

其中10vw和浏览器视窗宽度有关,remhtmlfont-size有关(默认为16px)。在10vw, 10rem, 100px三个值中其中前两个是相对长度值,最后一个是固定长度值。因此,就该示例来说,width计算值最大就是100px。而width真实的值和视窗宽度以及html元素的font-size有关,假设浏览器视窗宽度处在1200px位置处,那么10vw对应的就是120px,假设htmlfont-size采用的是默认值(16px),那么10rem对应的值是160px。在这样的上下文信息中min(10vw, 10rem, 100px)对应的就是min(120px, 160px, 100px),即,最后width的值为100px

同样这个示例,如果视窗宽度处在760px位置,那么10vw对应的值就是76px,同时假设htmlfont-size显式的修改为12px,那么10rem对应的就是120px,这个时候min(10vw, 10rem, 100px)对应的值就是min(76px, 120px, 100px),即min()函数最终返回的值就是10vw(即浏览器视窗宽度处在760px位置处时,76px)。

对于vwvhvminvmaxrem这样的相对单位计算起来还是很简单,他们的依赖关系很明确。但对于像%em这样的相对单位,那和环境之间的关系就很紧密了。

前面也提到过了,min()max()clamp()函数可以像calc()函数一样,使用数学表达式。但有一个细节需要注意,如果表达式中用到加法(+)和减法(-)时,其前后必须要有空格;对于乘法(*)和除法(/)前面可以没有空格。但为了避免这样书写方式引起的错误,个人建议平时在写代码的时候,在运算符前后都留一定的空格。

除了可以在函数中使用数学表达多之外,还可以嵌套使用,比如:

.element {
    width: max(100px, min(50vw, 500px));
    border: min(10px, calc(2px * 1vw)) solid #f36;
    box-shaodw: max(2vh, var(--x)) min(2vh, var(--y)) 0 rgba(0,0,0,.25)
}

嵌套层级越深越易造成错误,因此在没有特性情况(非必要)之下,不建议在函数中嵌套函数。如果要用到多个参数时,建议使用clamp()

我们使用min()max()clamp()函数主要是用来确保值不超过“安全”限制。例如,font-size使用了视窗单位(比如vw),但为了可访问性,会设置一个最小的值,确保文本可阅读。这个时候,我们可以像下面这样使用:

body {
    font-size: max(10 * (1vw + 1vh) / 2, 12px);
}

我们在使用min()max()函数时,偶尔也会造成一定的混淆,比如在max()函数中对某个值设置了最小值(即,像min-width这样的属性有效的使用max());同样的在min()函数中设置了最大值(即,像max-width这样的属性有效的使用了min())。为了偏于理解和可读,使用clamp()函数会更自然些,因为数值介于最小值和最大值之间:

body {
    font-size: clamp(12px, 10 * (1vw + 1vh) / 2, 100px);
}

前面我们对clamp()计算做过详细的介绍

但要注意,比如clamp()函数中值有“顺序错误”,即它的最小值(MIN)超过了最大值(MAX)。即 clamp(100px, 50vw, 50px),它会被解析为100px。针对这个示例,我们来看看它的计算过程。

当浏览器视窗宽度处在170px位置时:

clamp(100px, 50vw, 50px) ➜ clamp(100px, 85px, 50px)

这个时候VAL大于MAX,但同时也小于MIN。前面的内容告诉我们,当VAL大于MAX时,clamp()函数返回的是MAX,但在这个示例中,VAL同时也小于MIN,按照前面所说的,VAL小于MIN时,clamp()函数会返回MIN。看上去没矛盾,但看上去又有矛盾。而事实上呢,这个场景之下,clamp()函数返回的的的确确是MIN的值,即100px

针对这种场景,规范也有过相应的描述:

  • MAX大于MINclamp(min(MIN, MAX), VAL, MAX),如果希望避免重复计算,可以将嵌套在clamp()中函数的参数反转过来,即clamp(min(MAX, max(MIN, VAL)))
  • MAXMIN顺序交换clamp(min(MIN, MAX), VAL, max(MIN, MAX))。不幸的是,没有一种简单的方法可以做到不重复MINMAX

这里有一点特别提出,在使用min()max()clamp()函数时,里面的值都应该显式指定值单位,即使是值为0也应该带上相应单位,比如:

.element {
    padding: clamp(0, 2vw, 10px);
}

上面的padding将会是无效的。

给设计带来的变化

对于现代Web设计师和开发者都不是一件容易的事情,因为随着众多不同终端的出现,要考虑的场景会越来越复杂。随着min()max()clamp()函数的到来,设计师估计在做设计的时候也可能要有一些思想上的变化。比如说,在这之前,设计师可能会根据不同的场景设计为元素设计不同的大小:

但现在示不久的将来,针对不同的场景会像下面这样来做设计:

拿实际案例来说吧,在响应式设计中,希望在不同的终端上,文本的字号有所不同,那么就可以像下面这样使用:

你可能也已经发现了,这和我们前面介绍的clamp()函数是非常的匹配:

body {
    font-size: clamp(16px, 5vw, 50px)
}

这样的设计对于一些需要动态改变值的场景非常有用。

案例

如果你坚持阅读到这里的话,你对min()max()clamp()函数有了一定的了解。接下来,我们来看一些简单的实例,希望通过这些实现能更好的帮助大家理解这几个函数。

布局中的运用

比如我们有一个两例布局,希望侧边栏在大屏幕下有足够宽的空间,但同时也希望它有一个最小的宽度。在这之前,我们可能会这样来写CSS:

aside {
    width: 30vw;
    min-width: 220px;
}

如果是用Grid布局的话,可能会运用minmax()函数:

.container {
    display: grid;
    grid-template-column: minmax(220px, 30vw) 1fr
}

而现在,我们可以使用max()函数来处理:

aside {
    flex-basis: max(30vw, 220px)
}

也可以将其赋值给width

如果将上面的示例换成Grid布局,可以这样写:

body {
    display: grid;
    grid-template-columns: max(30vw, 220px) 1fr;
    column-gap: max(4vw, 20px);
}

动态设置字号

在《给CSS加把锁》一文中,我们知道可以通过CSS锁来设置font-size。简单地说,当视窗宽度小于320px时,font-size值为20px,当视窗大于960px时,font-size的值为40px,介于320px ~ 960px之间,font-size会根据视窗来动态改变。

这看上去和我们今天聊的clamp()函数非常的相似。也就是说,如果你要动态的设置font-size时,就可以采用clamp()函数。比如下面这个示例,我们给标题设置font-size,最小值为20px,最大值40px,然后给出一个推荐值(首选值),该值不会超过最大值,也不会低于最小值。

h1 {
    font-size: clamp(20px, 3vw, 40px)
}

事实上,仅使用3vw作为字体大小的首选值并不好。当用户在浏览器中缩放时,它将导致可访问性问题。不过我们可以使用下面的代码来避免:

h1 {
    font-size: clamp(20px, (1rem + 3vw), 40px)
}

平滑渐变

当在CSS中使用渐变时,可能希望渐变也能随着容器大小变化时,渐变的颜色也能平滑过渡。比如下面这样的一个示例:

body {
    width: 100vw;
    min-height: 100vh;
    background: linear-gradient(135deg, #ff9800, #ff9800 60%, #00bcd4);
}

明显可以看出它们之间的差异。虽然说可以借助CSS媒体查询来做一些优化,但这并不是一个最佳的方案。从今天开始,我们可以使用min()函数,让渐变变得更平滑:

body {
    background: linear-gradient(135deg, #ff9800, #ff9800 min(20vw, 60%), #00bcd4);
}

你可以尝试着改变视窗大小,看到的效果会和前面不一样:

另外,在我们实际开发中还有这样的场景,就是在图片上添加一层黑色到透明的渐变,让图片上的文本更易于阅读。实现这样的效果,我们可以像上例一样,使用max()函数来动态调整透明色的位置。

.element {
    background: linear-gradient(to top, #000 0, transparent max(20%, 20vw));
}

min()max()clamp()函数,还可以运用到很多地方,感兴趣的同学自己可以尝试一下。特别是在一些需要动态改变值,需要有一个要通过比较选择值的场景,非常适用。

小结

文章中聊到的min()max()clamp()CSS众多函数中的一部分,也被称为比较函数(也有人称之为计算函数)。它们便于我们为属性动态设置属性值,而且可以很好的来替代以前的min-widthmax-width以及媒体查询。更厉害的是,它们可以运用于任何尺寸属性,位置属性之上。相比于min-*max-*更为灵活。

最后希望这篇文章对大家有所帮助,如果你在这方面有更好的建议和相关经验,欢迎在下面的评论中与我们共享。

返回顶部