CSS中的动态计算

发布于 大漠

自从CSS的calc()函数得到浏览器的支持起,在CSS中就可以做一些简单的数学运算。如果你阅读过 图解CSS系列 中的 《CSS函数》一文的话,你会发现现在或将来有更多的函数可以直接帮助我们在CSS做一些计算,比如颜色计算、三角函数的计算等。尤其是CSS的min()max()clamp()以及CSS Grid布局模块中的minmax()出现之后网页布局带来了革命性的变化。除此之外,在一些特定的环境之下,还有一些其他的CSS函数可以帮助我们做一些动态计数和计算。今天在这篇文章中就来和大家聊聊这方面的话题。

回顾一下

CSS中的函数有很多种:

但我们今天主要和大家探讨其中的几个:

  • 数学函数calc():使用calc()函数可以在CSS中进行数学运算,可以结合CSS的单位一起计算,比如calc(100vw - 300px)
  • 比较函数min()max()clamp()min()max()函数可以接受两个或多个参数,并返回最小或最大的参数(值);clamp()min()max()的超集(组合),它接受三个参数,即clamp(MIN, VAL, MAX),等同于min(max(MIN, VAL), MAX)max(MIN, min(VAL, MAX))
  • 网格函数minmax():它接受两个参数,即minmax(MIN, MAX),该函数是CSS Grid布局模块中独有的一个函数,通常和另外一个网格函数repeat()运用在grid-template-columns属性上
  • 计数函数counter():该函数用来设置插入计数器的值,常和CSS的counter-resetcounter-increment属性以及伪元素::before::after::marker配合content生成计数器内容,比如实现列表项序列号,大纲列表等

另外,在接下来的内容中会涉及到CSS自定义属性(变量)相关的内容,如果你从未接触过的话,建议你先花一点点时间阅读《图解CSS:CSS自定义属性》和《CSS自定义属性你知多少》,这样更利于你阅读接下来的内容。

更多有关于CSS自定义属性的介绍还可以点击这里阅读

相对而言,其中min()max()clamp()是较新的CSS特性,为此先花一点时间简单向大家介绍一下这三个函数。

min()max()clamp()三个函数都常称为CSS的比较函数,其中min()max()相对而言要比clamp()函数易于理解。

min()max()

min()max()都可以接受两个或两个以上的参数,参数之间用逗号分隔,而且参数还可以是一些表达式,比如说一些简单的计算5vw + 5px,并且这些计算表达式不需要使用calc()函数。当然,你还可以在一个CSS变量里面进行计算,然后使用这个变量即可。这两个函数可以运用于可接受<length><frequency><angle><time><percentage><number><integer> 类型值的属性。

有关于CSS值和单位方面更详细的介绍,可以阅读《CSS 的值和单位》和 《元素尺寸的设置》。

min()max()之间的差异只是返回值的不同:

  • min()函数会从多个参数(或表达式)中返回一个最小值作为CSS属性的值,即 使用min()设置最大值,等同于max-width
  • max()函数会从多个参数(或表达式)中返回一个最大值作为CSS属性的值,即 使用max()设置最小值,等同于min-width

比如下面这个是min()函数的示例:

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

这个是max()函数的示例:

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

你可以尝试着在浏览器查看这两个示例,并且改变浏览器视窗的大小,你将看到.element的变化

clamp()

clamp()min()以及max()略有不同,它将返回一个区间值,即 在定义的最小值和最大值之间的数值范围内的一个中间值。该函数接受三个参数:

  • 最小值(MIN),
  • 中间值(VAL),也称首选值
  • 最大值(MAX

clamp(MIN, VAL, MAX),这三个值之间的关系(或者说取值的方式):

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

比如下面这个示例:

.element { 
    /** 
    * MIN = 100px 
    * VAL = 50vw ➜ 根据视窗的宽度计算 
    * MAX = 500px 
    **/ 
    width: clamp(100px, 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()max()函数都可以随着浏览器视窗宽度的缩放对值进行调整,但它们的计算的值取决于上下文。

特别声明,有关于这三个参数更详细的介绍,可以阅读《聊聊min()max()clamp()函数》一文。

动态计算

前奏有点长,下面开始来聊CSS中的动态计算,我们先从calc()函数开始。

calc()

大家对calc()函数的第一印象就是使用它来做一些计算。在一些布局场景中,calc()能帮助我们快速解决一些麻烦,甚至是实现一些令人感到头疼的问题。比如说,我们在构建一个APP应用的,时常会碰到NavBar均分的效果,如果NavBar的项目数是一个偶数项的话,计算非常简单,但对于一些奇数项来说,让我们无法均分完。拿三等分为例吧,将100%均分成三份的话,将永远无法均分完:

你可能首先会想到Flexbox布局中的flex: 1CSS Grid布局中的grid-template-columns: repeat(3, 1fr)来实现,正如《你可能不太熟知的布局技巧》文章中介绍的布局示例。这两种方案在某些情况之下是OK的,但当其带有文本,并且某一个文本的长度大于其容器的宽度时,将会打破其均分的状态:

这个时候我们使用calc()就可以避免打破均分的效果:

.nav__item {
    width: calc(100% / 3);
}

你可能也发现了,这样做也有一定的缺陷,那就是文本内容过长时会溢出。如果不希望溢出的话,则需要考虑使用CSS的text-oveflow:ellipsis用三个点来表示被截取的内容。

我们还可以使用CSS的自定义属性来代表NavBar的项目的数量,这样就可以灵活的根据项目数做计算:

:root {
    --i: 3;
}

.nav__item {
    width: calc(100% / var(--i));
}

也可以稍微借助JavaScript,根据NavBar的项目数动态设置--i的值:

const itemNums = document.querySelectorAll('.flex__item').length
document.documentElement.style.setProperty('--i', itemNums)

这里插点额外的东东。前面提到过,当在NavBar项目上内容超长时,即使显式设置flex:1会也会被长内容打破均分状态,如果希望避免被打破的话,有一个小技巧,只需要在Flex项目上显示设置min-width: 0即可。

.flex__item {
    min-width: 0;
}

.flex__item span {
    padding: 0 5px;
    font-size: 1.2rem;
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
    min-width: 0;
    width: 100%;
}

接下来,来看一个非常有意思的案例。在以往我们要动态调整元素的大小,很多时候会依赖于CSS的媒体条件,在不同的断点下调整元素的大小。其实,我们可以直接使用calc()函数在脱离媒体查询也可以实现动态缩放HTML元素大小。这功能随着vw视窗单位的出现就变得越来越受开发者的喜爱。而且这个功能有一个专业的术语,即 CSS锁(CSS Locks)

CSS锁是一种响应式的Web设计技术,他主要致力于解决响应式设计的文本排版,它允许你根据当前的视窗大小大两个值之间平稳地转换,而不是直接从一个值跳到另一个值。

CSS锁是@Mike Riethmuller在2015年的《Precise control over responsive typography》中首次展示的,但早在2012年的时候@Tim Brown在《Flexible typography with CSS locks》中就提出了该概。

@Tim Brown用了实际生活中的一个示例来描述CSS锁。在运河和河流中都会备有船闸用来控制河中水位,也可以在不同水位的水域之间升降船只:

不过大多时候该技术只是运用于font-size上。其实该技术还可以运用于任何需要数值的CSS属性上。从font-size,到width,再到box-shadow等。

注意,该技术除了使用calc()函数之外还需要紧密的结合视窗单位vw(或vh),并且该技术有一个核心的计算公式:

calc([min size]px + ([max size] — [min size]) * ((100vw — [min viewport's width]px) / ([max viewport's  width] — [min viewport's  width])))

先来看calc()函数的左侧部分,即, [min size]px 。它主要用来给元素设置一个最小的尺寸(即元素最小尺寸),其单位是px。这样无论怎么缩放元素的最终尺寸都不会是0。如果你希望元素最小尺寸是240px时,那么就可以将[min size]值设置为240,即[min size]px = 240px,并且将其放到calc()公式中:

// [min size] = 240
calc(240px + ([max size] — [min size]) * ((100vw — [min viewport's  width]px) / ([max viewport's  width] — [min viewport's  width])))

calc()函数后半部分有点复杂,我们拆分出来看。先看([max size] — [min size])这个部分。我们希望元素在一个尺寸范围内,比如最小[min size]到最大[max size]这个范围。而这两者的差值将会作为一个 倍数。假设我们希望元素尺寸范围是在240px ~ 480px范围内,那么[min size] = 240[max size] = 480,对应的([max size] - [min size])即是:

// [min size] = 240
// [max size] = 480
([max size] - [min size]) = (480 - 240)

将其放到calc()中:

// [min size] = 240
// [max size] = 480
calc(240px + (480 — 240) * ((100vw — [min viewport's  width]px) / ([max viewport's  width] — [min viewport's  width])))

再来看乘号(*)后面的这个部分((100vw — [min viewport's width]px) / ([max viewport's width] — [min viewport's width])),也是calc()中最后面的一部分。这里我们可以给浏览器视窗大小设置一个范围,即 [min viewport's width] ~ [max viewport's width],其中[min viewport's width]是视窗最小值,[max viewport's width]是视窗最大值。假设你期望的视窗宽度是576px ~ 1400px范围内,那么[min viewport's width] = 576[max viewport's width] = 1400,即:

// [min viewport's width] = 576
// [max viewport's width] = 1400
((100vw — [min viewport's  width]px) / ([max viewport's  width] — [min viewport's  width])) = ((100vw - 576px) / (1400 - 576)) 

这将根据浏览器视窗的宽度大小创建一个比例。任何超出576px ~ 1400px范围的东西都会继续分别向上或向下以线性的速度进行缩放。在这个基础上做一些简化的事情,就是插入一些数值,看看计算出来的比例值。我们使用一些常见的媒体断点的值来替代100vw,比如:

// »576px
((576px - 576px) / (1400 - 576)) = 0 / 824 = 0

// »768px
((768px - 576px) / (1400 - 576)) = 192 / 824 = 0.2330097

// »992px
((992px - 576px) / (1400 - 576)) = 416 / 824 = 0.5048543

// »1200px
((1200px - 576px) / (1400 - 576)) = 624 / 824 = 0.7572815

// »1400px
((1400px - 576px) / (1400 - 576)) = 824 / 824 = 1

如果我们将之前设置的元素尺寸(即,(480 - 240))乘以这个比例,就可以得到一个基于浏览器视口尺寸的元素尺寸的动态值:

// » 576px
(480 - 240) * ((576px - 576px) / (1400 - 576)) = 240 * 0 = 0px

// » 768px
(480 - 240) * ((768px - 576px) / (1400 - 576)) = 240 * 0.2330097 = 55.92px

// » 992px
(480 - 240) * ((992px - 576px) / (1400 - 576)) = 240 * 0.5048543 = 121.17px

// » 1200px
(480 - 240) * ((1200px - 576px) / (1400 - 576)) = 240 * 0.7572815 =181.75px

// » 1400px
(480 - 240) * ((1400px - 576px) / (1400 - 576)) = 240 * 1 = 240px

最后在这个基础上加上元素最小尺寸值,即[min size]px = 240px

// » 576px
240px + (480 - 240) * ((576px - 576px) / (1400 - 576)) = 240px + 240 * 0 = 240px + 0px = 240px

// » 768px
240px + (480 - 240) * ((768px - 576px) / (1400 - 576)) = 240px + 240 * 0.2330097 = 240px + 55.92px = 295.92px 

// » 992px
240px + (480 - 240) * ((992px - 576px) / (1400 - 576)) = 240px + 240 * 0.5048543 = 240px + 121.17px = 361.17px

// » 1200px
240px + (480 - 240) * ((1200px - 576px) / (1400 - 576)) = 240px + 240 * 0.7572815 = 240px + 181.75px = 421.75px

// » 1400px
240px + (480 - 240) * ((1400px - 576px) / (1400 - 576)) = 240px + 240 * 1 = 240px + 240px = 480px

也就是说,如果我们希望元素的宽度会是:

  • 浏览器视窗宽度为576px时,元素的宽度为240px
  • 浏览器视窗宽度为1400px时,元素的宽度为480px

可以像下面这样使用:

.element {
    width: calc(240px + (480 - 240) * ((100vw - 576px) / (1400 - 576)))
}

来看一个具体的示例:

如果你希望继续深入的了解这方面的知识,可以阅读前期整理的一篇文章《给CSS加把锁》。

要是你阅读过《CSS自定义属性的使用实例》一文,你会发现在文章中很多示例都有calc()的身影,比如说使用它来动态计算颜色:

:root { 
    --h: 80; 
    --s: 20; 
    --l: 60; 
    --threshold: 65; 
    --border-threshold: 80; 
} 

.button { 
    --hue: var(--h); 
    --switch: calc((var(--l) - var(--threshold)) * -100%); 
    --border-light: calc(var(--l) * 0.7%); 
    --border-alpha:calc((var(--l) - var(--border-threshold)) * 10); 
    
    background: hsl(var(--hue), calc(var(--s) * 1%), calc(var(--l) * 1%)); 
    color: hsl(0, 0%, var(--switch)); 
    border:.2em solid hsla(var(--hue), calc(var(--s) * 1%), var(--border-light), var(--border-alpha)); 
} 

.button--secondary { 
    --hue: calc(var(--h) + 60); 
}

还要以使用它来实现aspect-ration的效果(实现宽高比缩放):

.rect { 
    --ratio-w: 4; 
    --ratio-h: 3; 
    --aspect-ratio: calc(var(--ratio-w) / var(--ratio-h)); 
    --width: 400; 
    width: calc(var(--width) * 1px); 
    height: calc(var(--width) * 1px / var(--aspect-ratio)); 
}

这个示例使用的是calc()和CSS自定义属性来实现aspect-ratio的特性,如果你使用的是现代浏览器的话就不必这么麻烦了,可以直接使用aspect-ratio。如果你对aspect-ratio特性感兴趣的话,可以阅读《使用CSS的aspect-ratio实现宽高比缩放》一文。

min()max()clamp()

这里不会详细介绍min()max()clamp()函数的具体使用,有关于这几个函数的具体分析可以阅读《聊聊min()max()clamp()函数》一文。在这一节我们主要和大家探讨在实际开发中如何使用这几个函数,以及在使用的时候他们存在什么边缘,需要注意什么?通过这些简单的实例,你可以更清楚的知道,这几个函数能在实际业务开发中能帮助我们做些什么?

先从简单的min()max()开始,因为clamp()是他们的超集,相对复杂一点。

假设我们使用min()函数给你元素的width属性设置了一个值,比如:

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

min()函数会从列表值中返回最小值,因此,.elementwidth值是1px

在W3C的 CSS值和单位模块Level 4(CSS Values and Units Module Level 4) 有过这样的一段描述:

An occasional point of confusion when using min()/max() is that you use max() to impose a minimum value on something (that is, properties like min-width effectively use max()), and min() to impose a maximum value on something; it’s easy to accidentally reach for the opposite function and try to use min() to add a minimum size. Using clamp() can make the code read more naturally, as the value is nestled between its minimum and maximum.

大致的意思是,在使用min()max()函数时,偶尔会有一点混淆点,那就是你使用max()给某个元素设置了一个最小值(应该像给元素设置了min-width),而使用min()会给某个元素设置一个最大值(类似于max-width)。很容易让开发者使用min()给元素设置一个最小值(min-width)。使用clamp()可以让代码在阅读和理解方面更自然,因为值被嵌套在其最小值和最大值之间。

来看一个简单的示例,示例中有三个div元素,其中前两个使用min()函数给其设置width值,最后一个使用max()函数给其设置width值:

.box1 {
    width: min(150px, 300px);
}

.box2 {
    width: min(50px, 150px, 300px);
}

.box3 {
    width: max(150px, 300px);
}

示例中三个divwidth分别是150px50px300px,最终效果如下:

上面的示例有一个明显的特征,非常明显的将带有单位的返回值作为属性的输入值,即 在任何需要单位的函数中使用它们。而在一些场景中是不需要带有单位的,比如说颜色值的描述。来看一个简单的示例,看如何使用min()max()函数来给hsl赋值,比如:

.element {
    background-color: hsl(
        min(50, 100, 150),
        max(50%, 75%),
        max(25%, 50%)
    )
}

你可有在自己的脑海中立马会想到hsl()的值是hsl(50, 75%, 50%)

要是将CSS自定义属性引入进入的话,我们还可以通过CSS的媒体查询可者JavaScript来改变CSS自定义属性的值,让我们在一些特定的条件之下动态的改变颜色,比如下面的示例:

:root {
    --base-hue: 60;
    --extreme-hue: 300;

    --base-saturation: 60%;
    --region-staturation: 40%;

    --base-light: 70%;
    --settled-light: 30%; 
}

@media only screen and (min-width: 600px) {
    :root {
        --extreme-hue: 1;
        --base-hue: 359;
    }
}

body {
    --start-color: hsl(
        min(var(--base-hue), var(--extreme-hue)),
        max(var(--base-saturation), var(--region-staturation)),
        min(var(--base-light), var(--settled-light))
    );
    --stop-color: hsl(
        max(var(--base-hue), var(--extreme-hue)),
        min(var(--base-saturation), var(--region-staturation)),
        max(var(--base-light), var(--settled-light))
    );
    background-image: linear-gradient(
        to right,
        var(--start-color),
        var(--stop-color)
    )
}

尝试手动拖动浏览器,改变视窗宽度将会看到下图这样的效果:

我们可以在上面的示例基础上去掉媒体查询那部分代码,使用JavaScript的.setProperty()来动态改变CSS自定义属性的值:

const root = document.documentElement;

function setRandomVariables() {
    const variablesArray = [
        {
            "--base-hue": 60,
            "--extreme-hue": 300,
            "--base-saturation": "60%",
            "--region-saturation": "40%",
            "--base-light": "70%",
            "--settled-light": "30%"
        },
        {
            "--base-hue": 75,
            "--base-saturation": "20%",
            "--region-saturation": "80%",
            "--base-light": "30%"
        },
        {
            "--base-hue": 50,
            "--region-saturation": "5%",
            "--settled-light": "10%"
        },
        {
            "--extreme-hue": 5
        }
    ];

    const selectedVariables = Object.entries(
        variablesArray[Math.floor(Math.random() * variablesArray.length)]
    );

    selectedVariables.forEach((cssvars) => {
        root.style.setProperty(cssvars[0], cssvars[1]);
    });
}

let intervalID = window.setInterval(setRandomVariables, 2000);

效果如下:

你或许会说,这有啥动态的。还不是依赖于CSS媒体查询或JavaScript动态的调整CSS自定义属性的值。嗯,是这样的,但也不全是。不知道你是否留意过,我们在《CSS自定义属性》一文中还提到了CSS自定义属性另一个特性,即 CSS自定义属性和calc()结合可以实现 if ... else 的效果。比如,我们有一个自定义属性--i,当:

  • --i的值为1时,表示真(即打开)
  • --i的值为0时,表示假(即关闭)

来看一个小示例,我们有一个容器.box,希望根据自定义属性--i的取值为01做条件判断:

  • --i的值为1时,表示真,容器.box旋转30deg
  • --i的值为0时,表示假,容器.box不旋转

代码可能像下面这样:

:root {
    --i: 0;
}

.box {
    // 当 --i = 0 » calc(var(--i) * 30deg) = calc(0 * 30deg) = 0deg
    // 当 --i = 1 » calc(var(--i) * 30deg) = calc(1 * 30deg) = 30deg
    transform: rotate(calc(1 - var(--i)) * 30deg))
}

.box.rotate {
    --i: 1;
}

或者

:root { 
    --i: 1; 
} 

.box { 
    // 当 --i = 0 » calc((1 - var(--i)) * 30deg) = calc((1 - 0) * 30deg) = calc(1 * 30deg) = 30deg 
    // 当 --i = 0 » calc((1 - var(--i)) * 30deg) = calc((1 - 1) * 30deg) = calc(0 * 30deg) = 0deg transform: rotate(calc((1 - var(--i)) * 30deg)) 
} 

.box.rotate { 
    --i: 0; 
}

整个效果如下图:

有关于这方面更详细的介绍,还可以阅读:

而在CSS中除了媒体查询之外,还可以借助CSS的伪类选择器让属性值做出相应的变化。还可以将CSS自定义属性的切换开关结合起来。

再来看clamp()吧。相比较而言,clamp()要比min()max()更易于理解。clamp()可以让我们在三个值(clamp()中的MINVALMAX)取出MIN ~ MAX之间的一个范围值。比如:

下图中是首先值(VAL)是50%,最小值(MIN)是300px和最大值(MAX)是800px。根据前面的介绍,clamp(MIN, VAL, MAX)的作用是:

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

在CSS中,单位取值为%时,会根据上下文来做出相应的计算(有关于这方面详细的介绍可以阅读《CSS中百分比单位计算方式》一文)。为了让大家更易于理解,将上图中的clamp(300px, 50%, 800px)运用于img,而且它的上下文是:

<!-- HTML -->
<body>
    <img src="photo.jpg" alt="image" /> 
</body>

/* CSS */

img {
    width: clamp(300px, 50%, 800px);
    height: auto;
}

假设:

  • 视窗的宽度是1100px,那么50%对应的是550px,它在VALMAX之间,那么img的宽度是550px
  • 视窗的宽度是1624px,那么50%对应的是812,它大于MAX,那么img的宽度是800px
  • 视窗的宽度是560px,那么50%对应的是280px,它小于MIN,那么img的宽度是300px

我们可以将这种技术用于响应式的布局中,比如说,我们要把一个页面容器的宽度设置在16rem ~ 70rem之间,而且容器首选宽度是90vw时,可以像下面这样应用:

.wrapper {
    width: clamp(16rem, 90vw, 70rem)
}

可以得到像下图这样的效果:

上图来自于《[Use CSS Clamp to create a more flexible wrapper utility](https://piccalil.li/quick-tip/use-css-clamp-to-create-a-more-flexible-wrapper-utility》一文。

大家还记得前面介绍的?使用calc()实现CSS锁的功能。有了clamp()会让事情变得更为简单。比如说:

  • 视窗小于或等于360pxfont-size(也可以是比的属性)值是1rem
  • 视窗在361px ~ 839px之间时font-size的值在1rem ~ 3.5rem间自动变化
  • 视窗大于或等于840pxfont-size值是3.5rem

使用clamp()实现这样的效果可以像下面这样做。先设置:

  • 最小字号,比如1rem
  • 最大字号,比如3.5rem
  • 最小视窗宽度,比如360px
  • 最大视窗宽度,比如840px

如果把单位都转换为rem的话,我们可以将视窗宽度都用rem来表示。CSS中的rem都是基于html元素的font-size来计算,一般情况之下,浏览器的htmlfont-size值是16px,那么视窗宽度的360px840px对应的值分别是22.5rem52.5rem

如果我们在一个坐标系统中来描述视窗宽度和字号之间的关系的话,可以用x坐标来表示视窗宽度,y的值表示font-size

可以用相应的数学公式来描述:

slope = (maxFontSize - minFontSize) / (maxWidth - minWidth)
yAxisIntersection = -minWidth * slope + minFontSize

根据上面计算公式,可以得到的斜率值为0.08333333333333333y轴处的交点值为-0.875:

slope = (3.5 - 1) / (52.5 - 22.5) = 2.5 / 30 = 0.08333333333333333
yAxisIntersection = -22.5 * 0.08333333333333333 + 1 = -0.875

根据斜率值和y轴处的交点值,可以计算出clamp()函数中的首先值:

VAL = yAxisIntersection[rem] + (slope * 100)[vw]

即:

VAL = -0.875rem + 8.333vw

这个时候clamp()的值为:clamp(1rem, -0.875rem + 8.333vw, 3.5rem)。即:

.element {
    font-size: clamp(1rem, -0.875rem + 8.333vw, 3.5rem)
}

要是和CSS自定义属性结合起来的话,可以像下面这样:

:root {
    --maxViewportWidth: 52.5; // 840px相对于16px计算出来的rem值
    --minViewportWidth: 22.5; // 360px相对于16px计算出来的rem值

    --minFontSize: 1; // 最小字号font-size的 rem值
    --maxFontSize: 3.5; // 最大字号font-size的 rem值

    --f-slope: ((var(--maxFontSize) - var(--minFontSize)) / (var(--maxViewportWidth) - var(--minViewportWidth)));
    --yAxisIntersection: (-1 * var(--minViewportWidth) * var(--f-slope) + var(--minFontSize))

    --clamp: clamp(
        var(--minFontSize) * 1rem,
        var(--yAxisIntersection) * 1rem + (var(--f-slope) * 100vw),
        var(--maxFontSize) * 1rem
    )
}

.element {
    font-size: var(--clamp)
}

这种方式同样可以像calc()函数一样,运用于任何接受<length><frequency><angle><time><percentage><number><integer> 类型值的属性。

另外 @Mathias Hülsbusch 在 《The Raven Technique: One Step Closer to Container Queries》介绍了一种叫作“乌鸦技术”(Raven Technique)。简单地说,将CSS的calc()min()max()clamp()函数与CSS自定义属性(条件判断和逻辑运算)结合在一起,实现一种优秀的布局效果,即 根据容器查询条件(Containeer Queries) 实现不同的布局效果,比如下面这样的效果:

在前面提到,CSS自定义属性可以让我们实现01之间的切换(也就是模拟JavaScript中的if ... else的效果)。如果将这个功能和CSS的clamp()函数结合在一起的话,可以让进度条在不同的百分值下有不同的颜色:

上图的示例来自于 @Yair Even Or 在《CSS Switch-Case Conditions》示例演示

@Yair Even Or 在《CSS Switch-Case Conditions》在文章中介绍了,使用clamp()calc()和CSS自定义属性实现的条件切换的效果。具体的代码可以查看作者在Codepen提供的示例:

在示例中,还使用到了一些 CSS Houdini 的技术,如果你从未接触过的话,建议你花点时间阅读下面两篇文章:

CSS Grid 中的 minmax()

在《你可能不太熟知的布局技巧》和《响应式网格布局》中都提到过,我们可以使用CSS Grid 布局模块中的minmax()函数 和 CSS Grid的repeat() 以及 auto-fit(或auto-fill等特性,在不依赖任何CSS媒体查询特性就可以实现响应式布局效果,特别是那种根据不同容器宽度实现不同的布局:

.grid__container {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 2vh;
}

效果如下:

W3C规范是这样描述minmax(MIN, MAX)的,即minmax()函数定义一个大于或等于MIN,小于或等于MAX范围的值。该函数一般只用于CSS Grid布局中的grid-template-columnsgrid-template-rows属性上。比如下面这个示例:

.grid__container {
    grid-template-columns: minmax(200px, 500px) 1fr 1fr;
}

注意,上图来自于 @shadeed9 的《A Deep Dive Into CSS Grid minmax()》一文。

从上图中不难发现,grid-template-columns中定义了一个三列网格,其中第一列的宽度是minmax(200px, 500px),其意思是列的最小宽度是200px,最大宽度是500px,即第一列的宽度是在200px ~ 500px之间。第二列和第三列是1fr,意味着这两列的宽度将会均分容器剩余下来的空间。

有关于CSS Grid中 fr 的介绍,可以阅读《CSS Grid带来的新单位:分数单位 fr》。

就该示例而言,如果视窗宽度小到一定程度的时候,就会出现滚动条,如下:

如果要达到示例中的效果,就需要使用repeat()函数,不过repeat()函数有一个特征,表示重复。网格列的宽度都是一样的,比如:

.grid__container {
    grid-template-columns: minmax(200px, 1fr) minmax(200px, 1fr) minmax(200px, 1fr);
}

换成repeat()可以写成:

.grid__container {
    grid-template-columns: repeat(3, minmax(200px, 1fr));
}

仅此还不行,还需要使用auto-fillauto-fit来替代repeat()函数表示重复的数量(比如示例中的3)。两者的区别是:auto-fit会扩展网格项,以填充网格容器的可用空间,而auto-fill不会扩展网格项,而将保留网格容器可用空间,也不会改变网格项的宽度。比如:

来看一个示例,@shadeed9 的《A Deep Dive Into CSS Grid minmax()》文中提到的一个示例:

希望达到的效果是,随着视窗宽度变化卡片都有一个最佳的效果。实现这样的一个效果,首先会想到minmax()

.wrapper {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    grid-gap: 1rem;
}

上面代码在很多场景之下都会有一个好的效果,但当视窗宽度小于250px时就会出现水平滚动条。在CSS中要解决这个问题,方案有很多种,比如媒体查询:

.wrapper {
    display: grid;
    grid-template-columns: 1fr;
    grid-gap: 1rem;
}

@media (min-width: 300px) {
    .wrapper {
        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    }
}

除了媒体查询之外,还可以使用前面提介绍的比较函数,比如min()

.wrapper {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 250px), 1fr));
    grid-gap: 1rem;
}

示例中minmax()min()结合在一起使用,它们所起的作用是:

  • 当视窗宽度小于250px,那么minmax()函数的第一个值将是父宽度的100%
  • 当视窗宽度大于250px,那么minmax()函数的第一个值将是250px

具体效果如下:

不过在使用minmax()函数时有两个细节要注意:

  • 如果minmax(MIN, MAX)MIN 大于 MAX,那么MAX将会被忽略,minmax(MIN, MAX)将会取MIN
  • 如果minmax(MIN, MAX)MIN 值是1fr 时,MIN值将是无效的,整个声明将被忽略;但它对MAX值有效

因此,在minmax(MIN, MAX)中的MIN使用%vw值是需要特别注意,它会根据父容器或视窗的宽度做计算,计算出来的值很有可能会比MAX值大,最终就有可能会取MIN的值。这个时候你将看到auto-fitauto-fill相同的效果。

如果想了深入了解minmax()的话,可以阅读@shadeed9 的《A Deep Dive Into CSS Grid minmax()》一文。

counter()counters()

在《伪元素能帮助我们做些什么》和 《聊聊CSS的::marker》提到过,使用CSS的counter()函数和CSS的content可以给HTML的任何元素提供类似ol列表的有序列表计数效果:

比如下面这个示例:

<!-- HTML -->
<div>
    <p>Lorem ipsum dolor ...</p>
    <p>Lorem ipsum dolor ...</p>
    <p>Lorem ipsum dolor ...</p>
    <p>Lorem ipsum dolor ...</p>
    <p>Lorem ipsum dolor ...</p>
</div>    

/* CSS */
p {
    counter-increment: section;
}

p:nth-child(even):before {
    content: counter(section);
}

p:nth-child(odd):before {
    content: counter(section);
}

效果如下:

上面的示例,我们使用counter()counter-increment实现计数。除此之外,可以直接在::marker伪元素的content中使用counter(list-item)counters(list-item, '.')

但是非列表元素,哪怕是设置了display:list-item,直接在::markercontent中使用counters(list-item, '.')所起的效果和我们预期的有所不同。如果在非列表元素的::markercontent中使用counters()达到我们想要的效果,需要使counter-reset先声明计数器标识符,然后counter-increment调用已声明的计数器标识符(回归到以前::before的使用)。具本的可以看下面的示例代码:

::marker {
    content: counter(list-item);
    padding: 5px 30px 5px 12px;
    background: linear-gradient(to right, #f36, #f09);
    font-size: 2rem;
    clip-path: polygon(0% 0%, 75% 0, 75% 51%, 100% 52%, 75% 65%, 75% 100%, 0 100%);
    border-radius: 5px;
    color: #fff;
    text-shadow: 1px 1px 1px rgba(#09f, .5);
}


.box:nth-child(2n) ::marker {
    content: counters(list-item, '.');
} 

.box:nth-child(3) {
    section {
        counter-reset: item;
    }

    article {
        counter-increment: item;
    }

    ::marker {
        content: counters(item, '.');
    } 
}

具体效果如下

除此之外,使用CSS Houdini的自定义属性,还可以做一些动态计数,比如下面这个效果:

小结

上面大家所看到的就是CSS中现在已经提供的一些动态计算的能力。特别是CSS自定义属性的到来之后,这方面的能力变得更强,当然也让CSS变得更难于理解,带来了一定的复杂性。比如我们可以使用CSS自定义属性帮助我们做条件判断,做简单的逻辑运算,甚至还可以实现随机效果。在后面将和大家一起探讨在CSS中如何实现随机运算,如果你对这方面感兴趣的话,欢迎关注后续的相关更新。