前端开发者学堂 - fedev.cn

如何通过CSS自定义属性给CSS属性切换提供开关

发布于 大漠

CSS自定义属性相关的教程在互联网上可以说是铺天盖地,从简单的介绍,到使用指南的整理,以及相关的经验之谈等等。时至今天而言,CSS的自定义属性是一项很成熟的CSS特性,在很多方面都可以给前端开发者带来诸多的益处。而且在现代浏览器中也得到了较好的支持。当然,虽然CSS自定义属性已得到很好的支持,但很多同学还在担心其是否可以运用于生产环境,甚至也有不少的同学还在排斥该特性。虽然如此,我还是想花点时间再和大家聊聊今天的主题。这个主题来自于@Ana tudor大神在去年年底发布的两篇文章,可以说是把CSS自定义属性运用的淋漓尽致。

@Ana tudor的两篇文章介绍的内容主要围绕着自定义属性如何实现给同一声明做两种状态的切换,从而实现不同的效果。整体的效果如下图所示:

是不是看上去有点类似于早期的CSS禅意花园一样呀。不知道大家看到上图的效果,是不是和我一样,有一种冲动,想看看怎么快速实现上图的效果。感兴趣的同学可以接着往下阅读。

这篇文章篇幅会较长,如果感兴趣,也可以阅读 @Ana tudor的文章:

如果想直接看Demo效果,可以点击这里查看@Ana tudor整理的Demo集合

这篇文章将能学到的技巧

文章标题其实已经告诉了我们今天这篇文章的目的:

一个CSS声明就能做到屏幕不同断点下的效果,另外在宽屏幕上,奇数和偶数项不同的效果

或者收缩和扩展的动画效果:

可能很多同学会纳闷,仅仅一个CSS声明(规则)就能实现上面的效果,有点不可能,太夸大了。事实上,只有我们想不到的,没有我们做不到的。这也就是CSS神奇之处:

这一切都要归功于CSS的自定义属性,该特性对于实现这些效果(或者其他效果)都非常地有用。在接下来的内容,不会对CSS自定义的基本使用做相关阐述,只会一步步的告诉大家如何使用CSS自定义属性来实现文章开头提到的效果。这也是我们今天的目的。

为什么说CSS自定义属性有用

CSS自定义属性最早的代名词是CSS变量,源于CSS相关的处理器。它的出现让我们编写CSS打开了一扇创意的大门。CSS自定义属性可以让我们在CSS中使用一些带有逻辑、数学运算的一些操作。

同样拿@Ana Tudor去年写的一个Loading案例为举例。这是一个阴阳旋转的效果。实现这个效果基于一个div元素和两个伪元素::before::after

<div class="sym"></div>

对应的CSS代码很简单:

$d: 65vmin;
$f: .5;
$t: 1s;

.sym {
    position: relative;
    width: $d; 
    height: $d;
    border-radius: 50%;
    background: linear-gradient(white 50%, black 0);
    animation: r 2*$t linear infinite;
    
    &::before, 
    &::after {
        --i: 0; // 最为关键的一部分
        position: absolute;
        top: 25%; 
        right: calc((1 - var(--i)) * 50%); 
        bottom: 25%; 
        left: calc(var(--i) * 50%);
        border: solid $d/6 hsl(0, 0%, calc(var(--i) * 100%));
        transform-origin: calc(var(--i) * 100%) 50%;
        transform: scale($f);
        background: hsl(0, 0%, calc((1 - var(--i)) * 100%));
        border-radius: 50%;
        animation: s $t ease-in-out calc(var(--i)*#{-$t}) infinite alternate;
        content: ''
    }
    
    &:after { 
        --i: 1; // 重置为1
    }
}

@keyframes s { 
    to { 
        transform: scale(2 - $f) 
    } 
}

@keyframes r { 
    to { 
        transform: rotate(1turn) 
    } 
}

整体的效果如下:

制作的详细步骤和相关原理,可以阅读@Ana在17年写的一篇博文《Creating Yin and Yang Loaders On the Web

示例中,我们有一个CSS自定义属性--i,他的值在10之间进行切换(其实我们今天要说的就是这个)。

上面示例中,两个伪元素使用了相同的backgroundborder-colortransform-originanimation-delay等,而且这些值都依赖于CSS自定义属性--i。两个伪元素最初的状态--i设置的值都是0,但::after--i后面变为1,从而动态地修改了backgroundborder-colortransition-orginanimation-delay属性的值(这些属性的值都是计算来的,依赖于CSS自定义属性--i)。

如果没有CSS自定义属性,我们就需要在::after上重置这些属性的值。这样一来增加了不少的代码量,也增加了我们维护的难度。

简而言之,自定义属性(--i)之间做了一个0和非零(即1)之间的切换。

这也好比程序中的一个开关,0对应的是false,代表关闭,非零1对应的是true,代表打开。这一切的一切好比一个开关,让我们在真(开)假(关)之间进行切换。那么这个开关在一般情况之下是如何工作的呢?

零和非零值之间的切换

正如上例所示,阴阳Loading动效是div的两个伪元素::before::after之间更改所有属性,而这些属性的更改都是从开关的一种状态(零(关))到另一种状态(非零(开))。

如果我们想让我们的值在开关关闭--i:0)和开关打开--i:1)进行切换,那么我们就要使用开关值var(--i)乘以它。比如,假设角度值是30deg,这是一个非零值,那么他们开关切换对应的值应该是:

  • 当开关切换到关闭状态,即--i:0,那么对应的值,使用calc()计算可得:calc(var(--i) * 30deg),即0 * 30deg = 0deg
  • 当开关切换到打开状态,即--i:1,那么对应的值,calc(var(--i) * 30deg),即1 * 30deg = 30deg

然后,我们想把上面的状态做一个切换:

开关在关闭状态(--i:0)时,其值是一个非零的值,而在开关打开状态(--i:1),对应的值是一个0值。我们只需要这样做即可:calc(1 - var(--i))值再乘以其值。

同样,角度值30deg对应的零和非零值就成下面这样了:

  • 当开关切换到关闭状态,即--i:0,那么对应的值是 calc( (1 - var(--i)) * 30deg),即 (1 - 0) * 30deg = 30deg
  • 当玒关切换到打开状态,即--i:1,那么对应的值是 calc((1 - var(--i)) * 30deg),即 (1 - 1) * 30deg = 0deg

用下图来说明这个概念,可能会更清晰一些:

在阴阳Loading效果中,border-colorbackground-color值都是采用了hsl()函数来表示。借助Bicone(由两个圆锥体组成,底面粘在一起)来直观地阐述**HSL**:

色相是一个圆盘,围绕着双锥体(Bicone),也就是在初始位置和结束位置360°给的颜色值是同一个值,即red。如下图所示:

有关于CSS颜色相关更详细的介绍可以阅读下面两篇文章:

而饱和度从0%100%,当饱和度为0%时(在Bicone的垂直轴上),色相都在同一水平面上,变得不在再重要,其颜色都是灰色。这里所指的“同一水平面”是指具有相同的高度。0%的黑色点到100%的白色点。当高度为0%100%时,色相和饱和度都不再重要了。即总是得到黑色的亮度值为0%,白色的亮度值为100%

就上面的动画示例,阴阳图就两个颜色,非白即黑,和色相以及饱和度无关。也就是说他们是在黑和白之间切换,即亮度在0%100%之间的切换

  • 当开关处于关闭状态(--i:0),那么亮度的值为calc(1 - var(--i) * 100%),即calc((1 - 0) * 100%) = 100%,颜色为白色
  • 当开关处于打开状态(--i:1),那么亮度的值为calc(1 - var(--i) * 100%),即calc((1 - 1) * 100%) = 0%,颜色为黑色

对于background-colorleftrightanimation-delay属性,我们可以使用同样的原理来做计算。实现零和非零值的切换。

两个非零值之间的切换

前面我们看到是非零值之间的切换。如果我们想在开关关闭(--i:0)得到一个非零的值;同时在开关打开(--i:1)得到另一个非零值。又应该如何实现呢?

接下来我们一起来看看,两个非零值之间如何进行切换。比如,我们希望一个元素的background-color在开关关闭状态时(--i:0)是#ccc颜色,在开关打开状态时(--i:1)是#f90

我们要做的第一件事是从十六进制颜色换到rgb()hsl()。因为在CSS中,十六进制的颜色是无法通过calc()这样的函数来计算的,所以建议采用rgb()hsl()的方式来进行管理。而且我个人更趋向于使用hsl()这种格式来管理你的颜色。

因此,我们使用以下函数提取hsl()的三个组件,这些组件等价于我们的两个值(关闭状态$c0: #ccc,打开状态$c1: #f90):

$c0: #ccc; // 关闭状态的值
$c1: #f90; // 打开状态的值

$h0: round(hue($c0) / 1deg);
$s0: round(saturation($c0));
$l0: round(lightness($c0));

$h1: round(hue($c1) / 1deg);
$s1: round(saturation($c1));
$l1: round(lightness($c1));

上面的代码中运用了Sass中的一些函数,这里不做相关阐述,如果感兴趣,可以阅读《Sass函数》和《Sass的颜色函数》。

根据开关的切换,我们可以得到:

  • 当开关关闭时(--i:0),backgroundhsl($h0, $s0, $l0)
  • 当开关打开时(--i:1),backgroundhsl($h1, $s1, $l1)

我们可以把两个背景写成:

  • 当开关关闭时(--i:0),backgroundhsl(1*$h0 + 0*$h1, 1*$s0 + 0*$s1, 1*$l0 + 1*$l1)
  • 当开关打开时(--i:1),backgroundhsl(0*$h0 + 1*$h1, 0*$s0 + 1*$s1, 0*$l0 + 1*$l1)

使用自定义属性--i进行切换,可以将background统一起来:

--j: calc(1 - var(--i));
background: hsl(calc(var(--j)*#{$h0} + var(--i)*#{$h1}), 
                calc(var(--j)*#{$s0} + var(--i)*#{$s1}), 
                calc(var(--j)*#{$l0} + var(--i)*#{$l1}))

这里使用了另一个自定义属性--j,来表示--i的余值。当--i0时,--j1;当--i1时,--j0。同样用下图来向大家展示,两个非零值(#ccc#f90)之间根据开关状态进行切换:

上面的公式适用于在任意两个HSL值之间进行切换。然而,在这个特殊的例子中,我们可以简化它,因为当开关关闭时(--i:0),我们有一个#ccc颜色。

在考虑RGB颜色模型时,#ccc具有相同的RedGreenBlue值(即:rgb(204, 204, 204))。在HSL颜色模型中,色调是无关紧要的(我们的灰色对于所有色调看起来都是一样的),饱和度总是0%,只有亮度才是重要的,这决定了我们的灰色是还是

在这种情况之下,我们可以始终保持非灰色值的色相(开关在开的状态时,我们有一个值$h1)。因为任何灰色值的饱和度(在关闭状态时是$s0)总是0%,所以用它乘以01总是得到0%。因此,var(--j) * #{$s0}最终的值总是0%,这样一来,上面的公式可以变得更为简单一些,如下所示:

--j: calc(1 - var(--i));
background: hsl($h1, 
                calc(var(--i)*#{$s1}), 
                calc(var(--j)*#{$l0} + var(--i)*#{d1l}))

类似地,在其他一些属性上也可以使用相似的计算公式。比如font-size的值在2rem(开关关闭时--i:0)和10vw(开关打开时--i:1)进行切换:

font-size: calc((1 - var(--i))*2rem + var(--i)*10vw)

相应的效果如下图所示:

接下来我们来弄清楚另一个问题,这个开关倒底是怎么触发的。

什么触发开关状态

倒底是什么触发了开关状态呢?从开到关,或者从关到开。触发开关,我们有以下几种方式进行选择:

基于元素的切换

这意味着,某些元素的开关是关闭的,而其他元素的开关是打开的。例如,这可以由奇偶性来决定。假设我们想要所有的偶元素都被旋转,并且有一个orangebackground-color,而奇数不做任何旋转,且background-color#ccc

.box {
    --i: 0;
    --j: calc(1 - var(--i));
    transform: rotate(calc(var(--i)*30deg));
    background: hsl($h1, 
                    calc(var(--i)*#{$s1}), 
                    calc(var(--j)*#{$l0} + var(--i)*#{$l1}));
    
    &:nth-child(2n) { 
        --i: 1 
    }
}

效果如下图所示:

类似上面的方式,我们可以借助不同的CSS选择器来控制开关的状态。我们也可以只对标题或有特定的属性的元素打开它。

基于状态的切换

这意味着当元素本身(或它的父元素或它以前的兄弟元素之一)处于一种状态时,关闭开关;当它处于另一种状态时,打开开关。在CSS控制元素状态的方式主要依赖于元素的状态选择器,比如::focus:hover:active:checked:focus-within:empty:placeholder-shown或者其他的伪类选择器(用于控制元素状态的伪类选择器)。

比如我们有一个<a>标签,当:hover:focus状态时,font-sizecolor都会在不同的两个值之间进行切换:

$c: #f90;

$h: round(hue($c)/1deg);
$s: round(saturation($c));
$l: round(lightness($c));

a {
    --i: 0;
    transform: scale(calc(1 + var(--i)*.25));
    color: hsl($h, $s, calc(var(--i)*#{$l} + (1 - var(--i))*100%));
    
    &:focus, 
    &:hover { 
        --i: 1 
    }
}

效果如下图所示:

基于媒体查询的切换

另一种可能就是,切于媒体查询来切换开关。比如下面这个示例,在不同断点时显示不同的文本颜色:

h5 {
    --i: 0;
    color: hsl($h, $s, calc(var(--i)*#{$l} + (1 - var(--i))*100%));
    font-size: calc(var(--i)*5vw + (1 - var(--i))*1rem);
    
    @media (min-width: 320px) { 
        --i: 1 
    }
}

效果如下:

实战

通过上面的学习,我们了解了怎么使用CSS的自定义属性来实现开关的切换:零和非零值的切换以及两个非零值之间的切换。又是如何触发开关的切换。接下来,咱们通过一些复杂的示例来帮助我们加深这方面知识的了解。毕竟只有通过实战才能明白其中真理。

搜索框的伸缩动效

先上要完的示例的效果:

实现上图效果的HTML模板非常简单:

<div class="search">
    <input id='search-btn' type='checkbox'/>
    <label for='search-btn'>显示搜索栏</label>
    <input id='search-bar' type='text' placeholder='w3cplus.com'/>
</div>

从可用性角度来思考,网站上有这样的一个搜索框可能不是最佳的,但这里我们使用这个示例主要用来解剖CSS自定义属性的开关切换。所以不必太纠结其他的一些事项。

我们将要实现的效果很简单:首先隐藏输入框<input type="text" />,然后<input type="checkbox">来切换文本输入框的显示和隐藏。复选框未选中时,文本输入框隐藏,反之则显示!

基本样式不做过多的阐述,一看代码大家都知道其中的原委:

*, :before, :after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    font: inherit
}

html { 
    overflow-x: hidden 
}

body {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100vw;
    min-height: 100vh;
    background: #252525
}

[id='search-btn'] {
    position: absolute;
    left: -100vh
}

这里关键的是[id="search-btn"]中运用的样式,让复选框不在屏幕上显示。对于复选框的样式,我们主要是通过label来替代,在我们的示例中,让其看上去是一个大的圆形按钮:

$btn-d: 5em;

.search {
    min-width: 400px;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: row-reverse;
}

[for='search-btn'] {
    display: block;
    overflow: hidden;
    width: $btn-d;
    height: $btn-d;
    border-radius: 50%;
    box-shadow: 0 0 1.5em rgba(#000, .4);
    background: #d9eb52;
    text-indent: -100vw;
    cursor: pointer;
}

此时你看到的效果如下图所示:

接下来,需要对搜索框进行美化:

$btn-d: 5em;
$bar-w: 4*$btn-d;
$bar-h: .65*$btn-d;
$bar-r: .5*$bar-h;
$bar-c: #ffeacc;

[id='search-bar'] {
    border: none;
    padding: 0 1em;
    width: $bar-w;
    height: $bar-h;
    border-radius: $bar-r 0 0 $bar-r;
    background: #3f324d;
    color: #fff;
    font: 1em century gothic, verdana, arial, sans-serif;
        
    &::placeholder {
        opacity: .5;
        color: inherit;
        font-size: .875em;
        letter-spacing: 1px;
        text-shadow: 0 0 1px, 0 0 2px
    }
        
    &:focus {
        outline: none;
        box-shadow: 0 0 1.5em $bar-c, 0 1.25em 1.5em rgba(#000, .2);
        background: $bar-c;
        color: #000;
    }
}

这个时候效果如下:

上面这些都是搜索框的搜索按钮最基本的样式,对于一个从事前端的同学而言,上面的一切都并没有什么难度。接下来才是实现动态搜索框效果的关键部分。

到目前为止,我们看到的效果是,搜索框右侧边缘和搜索按钮左侧边缘刚好接触,而我们要的效果却是,按钮和搜索框有一点重叠。从示例上看,搜索框和搜索按钮是垂直居中的,并且有部分重叠。这样的一个效果,对于Flexbox布局并不是件复杂的事情,我们只需要在.search容器中设置align-items: center即可搞定。更为关键的是应该怎么重叠。

根据上面的代码我们可以得知,搜索按钮的宽度是$btn-d,这样一来,我们就可以计算出按钮(此示例它是一个圆形)的半径是$btn-d / 2(即.5 * $btn-d)。如此一来就可以设置搜索框margin-right-.5 * $btn-d。为了避免搜索框输入的文本内容不和搜索按钮重叠(被按钮遮住),需要给搜索框设置padding-right.5 * $btn-d。如下图所示:

对应的代码如下:

$btn-r: .5*$btn-d;

[id='search-bar'] {

    margin-right: -$btn-r;
    padding: 0 calc(#{$btn-r} + 1em) 0 1em;
}

这个时候你所看到的效果如下,只不过是搜索框在搜索按钮之上:

修复上图这样的问题,很简单:

[for='search-btn'] {
    position: relative;
}

接下来我们应该考虑搜索栏展开和收缩两状态下的总宽度:

  • 搜索栏在展开状态,总宽度是搜索框的宽度$bar-w加上搜索按钮的半径$btn-r(根据前面所介绍的,搜索框和搜索按钮有一部是重叠在一起,重叠的部分就是搜索按钮的半径,即$btn-r
  • 搜索栏在收缩(折叠)状态,总宽度就是搜索按钮的直径$btn-d

用图来描述的就如下图所示:

因为搜索栏在折叠状态时保持相同的中轴,因此需要将按钮向左移动,其移动的距离是:.5*($bar-w + $btn-r) - $btn-r。我们把该值赋值给一个变量$x,只不过按钮向左移动,需要的是一个负值-$x。同样的搜索栏在展开状态,按钮向右移动$x

前面也提到过了,对于该效果,复选框选中,搜索栏展开,反之则收叠。对于按钮和搜索框的移动,我们是通过transform中的translateX()来实现,他们的值都是$x,只不过一个是正值,一个是负值。为了做到这一点,我们设置一个CSS自定义属性--i,该自定义属性就是我们前面提到的切换开关。当开关处于关闭状态(--i:0),两个老都将被移动且复选框没有被选中;当开关处于打开状态(--i:1),搜索栏和搜索按钮都处一颗它们当前所占据的位置,没有移动,这个时候复选框被选中。

// 移动距离
$x: .5*($bar-w + $btn-r) - $btn-r;

[id='search-btn'] {
    position: absolute;
    left: -100vw;
        
    ~ * {
        --i: 0;
        --j: calc(1 - var(--i)) /* 当--i是0时--j为1;反之则为0*/
    }
        
    &:checked ~ * { 
        --i: 1 
    }
}

[for='search-btn'] {

    // 当--i是0, --j是1 => 按钮向左移动的值 -$x 
    // 当--i是1, --j是0 => 按钮移动的值是0
    transform: translate(calc(var(--j)*#{-$x}));
}

[id='search-bar'] {
    // 当--i是0, --j是1 => 搜索框移动的距离是 $x
    // 当--i是1, --j是0 => 搜索框移动的距离是 0
    transform: translate(calc(var(--j)*#{$x}));
}

此时的效果如下图所示:

因为搜索框上设置了transform,就建立了一个堆栈上下文。所以你看到的效果,搜索框层级总是在按钮之上,这样造成搜索按钮不易点击。修复这个小bug非常的简单,只需要在搜索按钮上添加一个z-index属性即可:

[for='search-btn'] {
    z-index: 1;
}

但这个效果离我们要的效果还很远。虽然复选框的选中和未选中对搜索栏进行扩展和折叠之间的转换。但最大的问题是,搜索框在折叠状态时,搜索框还是能看到。为了解决这个问题,我们就要使用到clip-pathinset()。它通过元素距边框的顶部、右侧、底部和左侧边缘之间的距离来指定一个剪切矩形。这个剪切矩形之外的所有东西都被裁剪掉。言外之意,inset()之外的东西未可见,反则可见。

如果你从未接触过clip-path相关的知识,建议你花点时间点击这里进行了解

从上图中可以得知,每个距离都从边框的边缘向内延伸。在这种情况下,它们是正的。但是它们也可以向外延伸,此时就是负的,并且矩形之外的东西都将会被裁剪掉。

为了让搜索框在获得焦点状态时(:focus)的阴影box-shadow不被裁剪,所以我们要让这个矩形有足够大的区域。也就是说这个矩形距离顶部dt、底部db和左侧dl的距离为负,而且要足够的大。另外还有一个dr,它的大小应该是搜索栏的全宽度$bar-w减去按钮的半径$btn-r,即$bar-w - $btn-r。当复选框未选中(关闭状态:--i:0)时,dr的值为$bar-w - $btn-r,当复选框选中(打开状态:--i:1),dr0

$out-d: -3em;

[id='search-bar'] {
    clip-path: inset($out-d calc(var(--j)*#{$bar-w - $btn-r}) $out-d $out-d);
}

此时的效果看上去有点生硬,我们可以给元素添加一个transition,让效果看起来更为生动和顺滑一点:

[id='search-btn'] {
        
    ~ * {
        transition: .65s;
    }
}

我们还可以使用相同的方式给搜索按钮设置不同的背景颜色,当复选框未选中(开关关闭--i:0)是绿色,复选框选中(开关打开--i:1)即展开状态,搜索按钮是粉色:

[for='search-btn'] {

    $c0: #d9eb52; // 折叠状态按钮颜色
    $c1: #dd1d6a; // 展开状态按钮颜色
    $h0: round(hue($c0)/1deg);
    $s0: round(saturation($c0));
    $l0: round(lightness($c0));
    $h1: round(hue($c1)/1deg);
    $s1: round(saturation($c1));
    $l1: round(lightness($c1));
    background: hsl(calc(var(--j)*#{$h0} + var(--i)*#{$h1}), 
                    calc(var(--j)*#{$s0} + var(--i)*#{$s1}), 
                    calc(var(--j)*#{$l0} + var(--i)*#{$l1}));
}

到现在为止,你所看到的搜索按钮只是一个带有背景颜色的(其中文字部分使用别的CSS代码,让用户看不到),所以我们接下来需要给按钮添加一些可识别的符号。比如说搜索栏在折叠状态(复选框未选中,开关处于关闭状态--i:0),搜索按钮有一个放大镜的图标;当搜索栏处于展开状态(复选框选中,开关处于打开状态--i:1),搜索按钮有一个x的图标。制作这样的图标,我们借助搜索按钮的两个伪元素::before::after来完成。我们首先要决定放大镜的直径以及放大径手柄线的长度:

$ico-d: .5*$bar-h;
$ico-f: .125;
$ico-w: $ico-f*$ico-d;

这里我们采用绝对定位,让图标在搜索按钮区域水平垂直居中,同时继承父元素transition的属性值。先来看放大镜的效果,在::before元素上设置了一个background(值是currentColor)制作放大镜的手柄,在::after上使用border-radius,让它变成一个圆形,再使用box-shadow来模拟放大镜的边框:

[for='search-btn'] {
    
    &:before, 
    &:after {
        position: absolute;
        top: 50%; 
        left: 50%;
        margin: -.5*$ico-d;
        width: $ico-d;
        height: $ico-d;
        transition: inherit;
        content: ''
    }
        
    &:before {
        margin-top: -.4*$ico-w;
        height: $ico-w;
        background: currentColor
    }
    
    &:after {
        border-radius: 50%;
        box-shadow: 0 0 0 $ico-w currentColor
    } 
}

现在怎么看也不像个放大镜,对吧。为了让图标看上去更上一个放大镜,两个部位(::before::after对应的部分)沿着x向两端延伸放大镜直径的四分之一(.25*$ico-d)。放大镜手柄(::before)沿着x轴向右移动.25 * $ico-d;放大镜圆圈部分(::after)沿着x轴向左移动.25 * $ico-d。两者的移动我们都可以使用CSS的translateX()函数来完成。为了让放大镜更形象一些,我们还需要将手柄水平方向做一些收缩,让其长度只有原来的一半长。注意其中一个细节的处理,需要注意transform-origin,在这个示例中是x轴点的100%位置处(transform-origin: 100% 50%)。

另外,手柄和圆圈的移动,仅仅是在搜索栏折叠的状态下发生,也就是复选框未选中(开关关闭时--i:0)。只不这里的切换是两个非零值之间的切换,因此额外增加一个新的CSS自定义属性--j。即:

  • 搜索栏折叠状态,复选框未选中,开关关闭:--i:0,对应的--j:1,位移的值为1*.25*$d = .25*$d,缩放因子为1 - 1*.5 = 1 - .5 = .5
  • 搜索栏展开状态,复选框选中,开关打开:--i:1,对应的--j:0,位移的值为 0*.25*$d = 0,缩放因子为1 - 0*.5 = 1 - 0 = 1

对应的代码:

[for='search-btn'] {
        
    &:before {
        transform-origin: 100% 50%;
        transform: 
            translate(calc(var(--j)*#{.25*$ico-d})) 
            scalex(calc(1 - var(--j)*.5))
    }
    
    &:after {
        transform: translate(calc(var(--j)*#{-.25*$ico-d}))
    } 
}

因为最终放大镜是旋转了一个45deg的角度。所以我们可以在搜索按钮本身上去做这个旋转:

[for='search-btn'] {
    transform: translate(calc(var(--j)*#{-$x})) rotate(45deg);
}

现在放大镜的效果是OK了,但是搜索栏在展开状态下,搜索按钮上的图标还是有点不对,我们需要的是一个类似x的图标。到目前为止,x图标的一部分由::before完成,它已经是好的,但另一条线,我们需要在::after上做一些调整。前面的代码告诉我们,::after最初是一个圆,然后box-shadow做的边框效果。现在我们要把这个圆变成一条线,意味着,需要沿着x轴将border-radius50%缩小到0%来实现。我们使用的比例因子是我们想要得到直线的宽度$ico-w与它在折叠状态下形成的圆的直径$ico-d之间的比。把这个值赋值给一个新的变量$icof,对应的比例因子为.125

放大镜是只在折叠下有,类似地,关闭图标只在展开状态下有,即复选框被选中,开关打开--i:1

  • 搜索栏折叠时,复选框未选中,开关关闭--i:0,此时--j:1,对应的border-radius1*50% = 50%,收缩比例为1 + 0*$ico-f = 1
  • 搜索框展开时,复选框选中,开关打开--i:1,此时--j:0,对应的border-radius0*50% = 0,收缩比例为0 + 1*$ico-f = $ico-f

::after上的代码相应做出调整:

[for='search-btn'] {
    
    &:after{
        border-radius: calc(var(--j)*50%);
        transform: translate(calc(var(--j)*#{-.25*$ico-d})) scalex(calc(var(--j) + var(--i)*.5))
    }
}

距离最终效果越来越近了。可以看到,上面的代码把box-shadow也收缩了。所以我们需要再添加一个内阴影来修复它:

[for='search-btn'] {
    --hsl: 0, 0%, 0%;
    color: HSL(var(--hsl));
        
    &:after{

        box-shadow: 
            inset 0 0 0 $ico-w currentcolor, 
            /* 搜索栏折叠: 复选框未选中, --i 是 0, --j是 1
             * 阴影扩大半径是 0*.5*$ico-d = 0
             * 透明度是 0
             * 搜索栏展开: 复选框被选中, --i 是 1, --j 是 0
             * 阴影扩大半径是 1*.5*$ico-d = .5*$ico-d
             * 透明度是 1 */
        
            inset 0 0 0 calc(var(--i)*#{.5*$ico-d}) HSLA(var(--hsl), var(--i));
    }
}

最终的效果如下:

响应布局

上一个示例我们可以说是一步一步的介绍了其实现的整个过程。接下来我们来看看使用这种技术怎么实现文章开头列的响应式布局的效果。但接下来的示例不会像上一个示例那样一步一步来阐述。只会把其中关键性的向大家介绍,即介绍它们背后的基本思想。

正如上面你所看到的示例,实际的段落元素p就是显示在每个区域最前面的部分,而带渐变的数字区域和后面大的灰色背景区域分别是使用::before::after来创建的。

另外数字区域的背景是独立的,在每个<p>元素上声明了一个CSS自定义属性--list,这个自定义属性上设置了两个颜色值,主要用来控制每个数字区块的背景颜色:

<p style='--slist: #51a9ad, #438c92'><!-- 第一个段落的文本 --></p>
<p style='--slist: #ebb134, #c2912a'><!-- 第二个段落的文本 --></p>
<p style='--slist: #db4453, #a8343f'><!-- 第三个段落的文本 --></p>
<p style='--slist: #7eb138, #6d982d'><!-- 第四个段落的文本 --></p>

上面示例是一个响应式布局的效果,在不同屏幕下会有不同的布局调整。比如宽屏、正常屏和窄屏下的效果是有所不同的:

这些计算都交给了CSS自定义属性去做,然后切换的开关呢交给了CSS的媒体查询:

html {
    --narr: 0;
    --comp: calc(1 - var(--narr));
    --wide: 1;
        
    @media (max-width: 36em) { 
        --wide: 0 
    }
        
    @media (max-width: 20em) { 
        --narr: 1 
    }
}

数字区域是绝对定位的,而且奇偶处理不同的位置,奇数居左,偶数居右。这里设置CSS自定义属性--parity来做为切换的开关,开关关闭时--parity:0,居左,开关打开时--parity: 1,居右。触发开关是通过结构选择器:nth-child(2n)来触发:

p {
    --parity: 0;
    
    &:nth-child(2n) { 
        --parity: 1 
    }
}

left:0时,数字区域的左侧边缘和其父容器(p)左侧边缘对齐(紧贴在一起),left: 100%时,数字区域左侧边缘和其父容器右侧边缘对齐(紧贴在一起):

为了使偶数的数字区域的右边缘和其父容器的右边缘对齐(紧贴在一起),那么left: 100%就需要从100%减去数字区域的宽度,此例为$num-d,这样一来,left的值就可以通过--parity一起来控制:

left: calc(var(--parity)*(100% - #{$num-d}))

但在宽屏的时候,数字区域的left并不是0100% - $num-d。奇数向左侧继续偏移了1em(开关打开--parity:0),偶数向右侧偏移1em(开关关闭--parity: 1)。现在的问题是如何切换?最简单的方法是使用-1的幂。但在CSS中并没有power()函数或这样的运算符。这也意味着,我们需要通过其他的数学计算方式的组合来实现,这样就产生了像下面这样奇怪的公式:

/*
 * 开关关闭 --parity: 0, 对应的值 1 - 2*0 = 1 - 0 = +1
 * 开关打开 --parity: 1, 对应的值 1 - 2*1 = 1 - 2 = -1
*/
--sign: calc(1 - 2*var(--parity))

如果把奇偶性和宽屏的参数添加进来,那就变成了:

/*
 * 开关关闭:--wide: 0, 不是宽屏
 * 开关打开:--wide: 1, 对应的宽屏
*/
left: calc(var(--parity)*(100% - #{$num-d}) - var(--wide)*var(--sign)*1em)

接着给元素设置一个宽度。普通的时候是容器的80%,窄屏的时候是100%,宽屏的时候,设置了一个max-width。控制元素最大宽度是多少。在这里也使用到了两个开关--comp(正常屏),窄屏--narr

width: calc(var(--comp)*80% + var(--narr)*100%);
max-width: 35em;

对于元素的font-size也是类似:

calc(.5rem + var(--comp)*.5rem + var(--narr)*2vw)

对于伪元素::after制作的大的矩形背景,其定位的偏移量也是如此:

right: calc(var(--comp)*#{$off-x}); 
left: calc(var(--comp)*#{$off-x});

最后提一点,就是每个区域的数字,比如0102之类的,并不是列表产生的,也不是在HTML中添加的。这里采用了CSS的counter()特性来完成的。首先在<p>元素上使用counter-increment定义一个顺序号名称:

counter-increment: idx;

然后在p::before中的content属性中,通过counter()来调用,同时给他设置一个样式:

content: counter(idx, decimal-leading-zero)

这样,浏览器就会根据元素在HTML中的顺序来产生相就的序列号。

如果你从未接触地counter()相关的知识,可以点击这里进行了解

到这里为止,通过CSS自定义属性设置一些开关,从而快速实现一个响应的布局就完成了。当然要实现上例的效果,还要添加一些别的样式代码。但有关于核心部分和背后的实现原理,上面的代码都基本上罗列出来了。感兴趣的同学可以仔细阅读一下案例的源码。或者像前面的示例一样,自己一步一步写一下,将会有更深的体会,也能更易的理解前面介绍到的一些技术原理。

接着再来看一个更为复杂一点的案例,这个案例采用了CSS的Grid布局。先上效果:

这个示例中每个列表项(article元素)设置了一个三行两列的网格系统(CSS Grid),只不过在不同屏幕上显示的形式有所不同,如下图所示:

每个article元素包含了一个h3h4p元素,同时h3h4有一个伪元素::before,每个元素对应的区域如下图所示:

这个布局中运用到的公式相对而言较为复杂一点:

// 宽屏幕下的相关公式
// $col-a-wide 用于二级标题和段落
// $col-b-wide 用于一级标题
$col-1-wide: calc(var(--q)*#{$col-a-wide} + var(--p)*#{$col-b-wide});
$col-2-wide: calc(var(--q)*#{$col-b-wide} + var(--p)*#{$col-a-wide});

// 普通情况下的相关公式
$row-1: calc(var(--i)*#{$row-1-wide} + var(--j)*#{$row-1-norm});
$row-2: calc(var(--i)*#{$row-2-wide} + var(--j)*#{$row-2-norm});
$row-3: minmax(0, auto);
$col-1: calc(var(--i)*#{$col-1-wide} + var(--j)*#{$col-1-norm});
$col-2: calc(var(--i)*#{$col-2-wide});

$art-g: calc(var(--i)*#{$art-g-wide});

html {
    --i: var(--wide, 1); // 1 in the wide screen case
    --j: calc(1 - var(--i));

    @media (max-width: $art-w-wide + 2rem) { 
        --wide: 0 
    }
}

article {
    --p: var(--parity, 0);
    --q: calc(1 - var(--p));
    --s: calc(1 - 2*var(--p));
    display: grid;
    grid-template: #{$row-1} #{$row-2} #{$row-3}/ #{$col-1} #{$col-2};
    grid-gap: 0 $art-g;
    grid-auto-flow: column dense;

    &:nth-child(2n) { 
        --parity: 1 
    }
}

同时我们设置了grid-auto-flow: column dense,这样也只能侥幸设置一级标题覆盖整个列,而二级标题和段落可以自由排列。

这里涉及到了CSS Grid中自动排列的算法,有关于这方面的详细介绍可以点击这里进行了解

// 宽屏时,奇数列表项: --i 是 1, --p 是 0, --q 是 1
// 对应的列: 1 + 1*1 = 2
// 宽屏时,偶数列表项: --i 是 1, --p 是 1, --q 是 0
// 对应的列: 1 + 1*0 = 1
// 窄屏时: --i 是 0,所以 var(--i)*var(--q) 是 0 以及对应的列是 1 + 0 = 1
grid-column: calc(1 + var(--i)*var(--q));

// 总是从第一行开始
// 宽屏时 --i: 1 对应的行是 1 + 2*1 = 3 
// 反之 --i :0 对应的行是 1 + 2*0 = 1 
grid-row: 1/ span calc(1 + 2*var(--i));

对于每个项目。其他一些属性取决于屏幕的断点:

$art-mv: calc(var(--i)*#{$art-mv-wide} + var(--j)*#{$art-mv-norm});
$art-pv: calc(var(--i)*#{$art-pv-wide} + var(--j)*#{$art-p-norm});
$art-ph: calc(var(--i)*#{$art-ph-wide} + var(--j)*#{$art-p-norm});
$art-sh: calc(var(--i)*#{$art-sh-wide} + var(--j)*#{$art-sh-norm});

article {
    margin: $art-mv auto;
    padding: $art-pv $art-ph;
    box-shadow: $art-sh $art-sh calc(3*#{$art-sh}) rgba(#000, .5);
}

在宽屏时,border-widthborder-radius是一个非零值:

$art-b: calc(var(--i)*#{$art-b-wide});
$art-r: calc(var(--i)*#{$art-r-wide});

article {
    border: solid $art-b transparent;
    border-radius: $art-r;
}

宽屏的时候,元素的宽度有是有限制的,但在其他情况之下,宽度是100%

$art-w: calc(var(--i)*#{$art-w-wide} + var(--j)*#{$art-w-norm});

article {
    width: $art-w;
}

对于奇偶性变化时,数字区域的渐变色方向填充方式也有差异:

background: 
    linear-gradient(calc(var(--s)*90deg), #e6e6e6, #ececec) padding-box, 
    linear-gradient(to right bottom, #fff, #c8c8c8) border-box;

有关于详细的代码,大家感兴趣的话,可以查阅示例代码源码。

上面的示例,我们学习到了如何通过一个CSS自定义属性如何驱动布局和交互的变化,但它们有一个共性:都是对数字进行切换,比如说零和非零的值或者两个非零值之间的切换。而且它们对于长度,百分比,角度,持续时间,频率和无单位数值等属性有效。事实上呢?我们还有很多CSS属性并和这些值无关,比如像flex-directiontext-align之类的属性,它们的值是一些关键词之间的切换,比如flex-direction对应的就是rowcolumn之类,而text-align对应的是leftright等。

接下来的示例,我们就来探讨这方面的知识点。

这将会涉及到CSS自定义属性的回退值和无效值两个方面。

CSS自定义属性的回退值

CSS自定义属性中,var()可以接受两个参数,第一个是调用已声明的自定义属性,第二个参数就是回退值,这是一个可选参数,当自定义属性无效时,将会调用第二个参数设置的值。比如:

:root {
    --color: #ccc;
}

.box {
    color: var(--color, #000)
}

另外,回退值也可以是另一个CSS自定义属性,它可以有一个CSS自定义属性的回退值。如此一来,这样就陷入了死循环之中:

background: var(--c, var(--c0, var(--c1, var(--c2, var(--c3, var(--c4, #ccc))))))

其次,回退值还可以是一个列表,只要用逗号分隔,都是有效的,比如:

background: linear-gradient(90deg, var(--stop-list, #ccc, #f90))

来看一个简单的示例:

还有就是,不同的地方,还可以使用不同的回退值:

div {
    
    background: linear-gradient(90deg, var(--stop-list, #ccc, #f90));
        
    &:nth-child(2) {
        background: linear-gradient(90deg, var(--stop-list, #c0c, #f00));
    }
}

无效值

无效值指的是声明的属性在计算的时候无效。比如:

--c: 1em;
background: var(--c)

1em是一个有效的长度值,但对于background-color属性来说却是是一个无效值。因此在这里将会用初始值transparent替代。

将回退值和无效值综合起来

来看一个简单的示例,假设我们有一些段落元素,其中更改颜色的亮度,让奇偶数段落的文本颜色在黑白之间进行切换。如果你仔细阅读了前面的内容,实现这样的效果,应该是一件非常轻易的事情。

p {
    --i: 0;
    /* 奇数段落,开关关闭 --i: 0 ,颜色亮度为 0*100% = 0% 对应颜色为黑色
    * 偶数段落,开关打开 --i: 1 ,颜色亮度为 1*100% = 100% 对应颜色为白色
    * /
    color: hsl(0, 0%, calc(var(--i)*100%));

    &:nth-child(2n) { 
        --i: 1 
    }
}

在这个基础上新增一个需求,奇数的段落文本向右对齐,而偶数段落文本向左对齐。为了实现这个需求,我们引入一个新的CSS自定义属性--parity,一般情况下不显式地给这个自定义属性设置值,只有偶数段落才设置。在一般情况下,我们要做的是集合上一个自定义属性--i,并将--i的值设置为--parity,同时给其设置一个回退值0

p {
    --i: var(--parity, 0);
    color: hsl(0, 0%, calc(var(--i)*100%));

    &:nth-child(2n) { 
        --parity: 1 
    }
}

到目前为止,上面的代码和前面所介绍的代码没有什么不同之处。但是,我们可以利用这个事实:在设置一个自定义属性时,可以添加一个回退值。前面也提到过,我们可以在不同的地方为同一个CSS自定义属性使用不同的回退值,那么给--parity的回退值,可以是0,也可以是right之类:

text-align: var(--parity, right)

如此一来,没有明确设置--parity时,text-align会采用回退值right。但是偶数段落,--parity的值为1,而1对于text-align是于个无效值,这个时候text-align将会采用其初始值left。这样就达到我们所需的效果:

上面的示例比较简单,为了更好的理解这方面的知识点,接下来看一个较为复杂一点的案例。

接下来这个示例中有五张卡片,每个卡片对应的是一个div,给它一个类名.card。通过一些基本样式,让每张卡片看上去好看一点。

接下来,使用CSS Counters(计数器)给每张卡片添加序列号:

.card {
    counter-increment: count;

    &::before {
        content: counter(count, decimal-leading-zero);
    }
}

现在,给卡片.card使用Flexbox布局,让卡片的序列号和卡片内容垂直居中:

.card {
    display: flex;
    align-items: center;

    &::before {
        font-size: 2em;
    }
}

前面都可以说是一些准备工作,接下来才是有兴趣的部分,也就是和今天学习的内容有关联的部分。我们来设置第一个开关--i。用来改变偶数卡片上数字的位置:

.card {
    // 设置数字区域顺序的开关
    // --i = 0, 开关关闭,数字区域的顺序 order=0
    // --i = 1, 开关打开,数字区域的顺序 order=1
    --i: 0;

    &::before {
        order: var(--i);
    }

    &:nth-child(2n) {
        --i: 1;
    }
}

为了让数字区域和文本内容有点间距,我们添加一个变量$gap。然后在.card::before上设置margin的值等于这个变量。奇数卡片数字区域居左,所以对应的是margin-right: $gap;而偶数卡片数字区域居中,所以对应的是margin-left: $gap。不管是margin-left还是margin-right,它们的值都是一个非零的值。如果我们要使用开关来进行切换,那应该对应的是前面所学的——两个非零值的切换。这里我们同样引入--i这个开关:

  • 开关关闭--i:0margin-left = calc(var(--i) * $gap) = 0 * $gap = 0;margin-right = calc((1 - var(--i)) * $gap) = 1 * $gap = $gap
  • 开关打开--i:1margin-left = calc(var(--i) * $gap = 1 * $gap = $gap);margin-right = calc((1 - var(--i)) * $gap) = 0 * $gap = 0

这样一来,对应的数字区域的margin-leftmargin-right可以轻松的进行切换

$gap: .75em;

.card {
    &::before {
        margin-left: calc(var(--i) * #{$gap});
        margin-right: calc((1 - var(--i)) * #{$gap});
    }
}

如果有多个地方要使用到这个互补值(1 - var(--i)),那么可以重新再定义一个开关:--j,其值为calc(1 - var(--i))。这样一来,上面代码可以修改成:

.card {
    // 设置关开 --i
    --i: 0;

    // 当 --i = 0 => --j = 1
    // 当 --i = 1 => --j = 0
    --j: calc(1 - var(--i));
    
    
    &:nth-child(2n) {
        --i: 1;
    }

    &::before {
        // --i等于0,开关关闭,数字的顺序为 order=0
        // --i等于1,开关打开,数字的顺序为 order=1
        order: var(--i);
        
        // 当 --i = 0 => margin-left = 0; --j = 1; margin-right = $gap
        // 当 --i = 1 => margin-left = $gap; --j = 0; margin-right = 0
        margin-left: calc(var(--i) * #{$gap});
        margin-right: calc(var(--j) * #{$gap});
    }    
}

接下来希望卡片有个背景颜色,比如说是一个灰色(#ccc)到橙色(#f90)的渐变颜色。同样的,奇数卡片渐变色是从左到右(#ccc => #f90),而偶数卡片是从右到左(#f90 <= #ccc)。同样是方向有一个切换。即 渐变颜色都是灰色到橙色,只不过奇数卡片是to right,而偶数卡片是to left。在CSS的渐变中,对于to right对应的刚好是90deg,反之,to left对应的是-90deg。如此一来,我们也可以借助--i这个开关来进行切换:

  • --i:0,开关关闭,渐变色#ccc#f90to right(也就是90deg
  • --i:1,开关打开,渐变色#ccc#f90to left(也就是-90deg

一个是正90deg,另一个是负90deg,也就是说他们的绝对值是相同的,都是90deg。前面也提到过了,在CSS中没有power()这样的函数,所以我们要额外的去做一个计算:

  • --i:0,奇数卡片,要做的是+1
  • --i:1,偶数卡片,要做的却是-1

根据前面所学,我们可以设置另外一个开关来做这件事:

// --i = 0 => 1 - 2 * 0 = 1 - 0 = 1
// --i = 1 => 1 - 2 * 1 = 1 - 2 = -1
--s: calc(1 - 2 * var(--i))

有了这个公式,渐变颜色的90deg-90deg就很好控制了:

.card {
    // --i = 0 => 1 - 2 * 0 = 1 - 0 = 1
    // --i = 1 => 1 - 2 * 1 = 1 - 2 = -1
    --s: calc(1 - 2 * var(--i));

    // 给渐变色设置一个自定义属性
    --color-list: #ccc, #f90;

    background:linear-gradient(
        calc(var(--s) * 90deg),
        var(--color-list)
    )
}

如果把前面border样式注释掉,现在看到的效果如下:

接下来,再给卡片添加一点transform样式:

  • 奇数卡片:translate(10%) rotate(5deg)
  • 偶数卡片:translate(-10%) rotate(-5deg)

这个和渐变实现方式是一样的:

.card {
    transform: translate(calc(var(--s)*10%)) 
            rotate(calc(var(--s)*5deg));
}

接着我们再给卡片添加圆角。同样的奇数卡片圆角在左边,偶数卡片圆角在右侧。只不过这里有一个小细节,由于我们并无法知道卡片的内容是多少,从而也无法确认卡片的高度是多少,如果圆角的半径想设置为卡片高度的一半,这无形之中是一个较大的难度,甚至是无法确定的值。所以这里的方案是给圆角预设一个较大的值,比如50vh

$r: 50vh;

.card {
    // --i = 0 => --j = 1, --r0 = $r, --r1 = 0
    // --i = 1 => --j = 0, --r0 = 0, --r1 = $r
    --r0: calc(var(--j) * #{$r});
    --r1: calc(var(--i) * #{$r});
    border-radius: var(--r0) var(--r1) var(--r1) var(--r0);
}

到这一步可以看到一定的效果了。但上面涉及到的都是与数值之间的计算。接着来一些不是数值之间的切换。比如,text-align属性,奇数卡片文本右对齐text-align:right,偶数卡片文本左对齐text-align。对于text-align这样的属性而言,和前面提到的属性都不一样,它的有效值都是一些关键词,比如leftright等。因此,在这里没有办法使用一些数学计算的技巧来帮助我们。

但幸运的是,我们可以使用CSS自定义属性另一个特性,在调用CSS自定义属性时设置一个回退值,关于这一点,前面也提到过。如果你没有任何印象的话,建议你重新回到前面的内容温故一下。为了实现text-align能根据不同的卡片(奇偶性)实现leftright之间的切换。我们新增一个自定义属性--p。在偶数卡片中将其设置为1。有一点不同之处,--p不会像--i一样,显式的设置一个值,因为我们希望这个变量的不同回退值用于不同的属性。

至于--i,我们也要略作一上调整,将其值设置为var(--p, 0),其中0作为--i的回退值。这个0是在一般情况下使用的值,因为我们从为没有显式地设置--p的值。在这个示例中,只有偶数卡片中显式的设置了--p的值为1。与此同时,text-align的被设置为var(--p, right),其中回退值为right。此时,对于偶数卡片时,--p的值为1,而这个1对于text-align属性又是一个无效值,因此这个时候text-align会初始值,即left。回过头来看奇数卡片,text-align: var(--p, right),因为在奇数卡片中没有显式地设置--p值,所以这个时候会采用自定义属性的回退值,即right。从而达到我们所要的目的:奇数卡片文本右对齐,偶数卡片文本左对方!

.card {
    --i: var(--p, 0);
    text-align: var(--p, right);

    &:nth-child(2n) {
        --p: 1;
    }
}

最近再添加一点响应式主面的功能。对于宽屏,上面的效果已经OK了,现在我们需要给窄屏下的卡片添加一点样式,让其看起来好看一些:

  • 窄屏下去掉卡片圆角效果,即border-radius重置为0
  • 窄屏下,卡片不做任何位移和旋转,需重置transform的值
  • 窄屏下,卡片区中数字顺序order和外距margin的重置
  • 窄屏布局不是横排,变成竖排,即flex-directionrow变成column
  • 窄屏下,卡片文本内容字号的调整

为了完成这个效果,重新引入另外两个开关(CSS自定义属性)--wide--k,主要用于宽屏和窄屏之间的切换:

.card {
    // 宽屏和窄屏的切换
    --k: var(--wide, 0);

    @media (min-width: 340px) {
        --wide: 1
    }
}

宽屏和窄屏时,卡片的border-radius会有所调整,也就是说--k会影响--r0--r1的值:

.card {
    --r0: calc(var(--k) * var(--j) * #{$r});
    --r1: calc(var(--k) * var(--i) * #{$r});
}

这个时候你拖动浏览器的大小,就可以看到类似下图的效果了:

接着把transformflex-direction等属性的值,也根据--k--wide开关来做相应的切换:

.card {
    transform: translate(calc(var(--k) * var(--s) * 10%))
            rotate(calc(var(--k) * var(--s) * 5deg));
    flex-direction: var(--wide, column);
    font: 900 calc(var(--k) * .5em + .75em) segoe script, comic sans ms, cursive;

    &::before {
        order: calc(var(--k) * var(--i));
        margin-left: calc(var(--k) * var(--i) * #{$gap});
        margin-right: calc(var(--k) * var(--j) * #{$gap});
    }
}

最终的效果如下所示:

拖动浏览器来改变浏览器大小,你会看到宽屏和窄屏下卡片不同的样式效果:

类似这样的技术实现的Demo效果还有很多。@Ana Tudor在Codepen上有一个Demo集合,感兴趣的同学可以自己去查看每个Demo的源码。当然,你也可以根据文章中介绍的内容,发挥你自己的创意,实现不同的效果。如果您有类似的经验和相关Demo,欢迎在下面的评论中与我们分享。

总结

这篇文章篇幅较大,如果您坚持阅读到这里,说明您已经阅读完全文了,对里面的内容也有所了解,而且我也相信您或多名少已经掌握了文章中提到的技术:如何通过CSS自定义属性,给CSS的属性值做开关切换,即零和非零,两个非零值的切换,甚至采用CSS自定义属性中的回退值和CSS属性的无效属性值的结合,还能做出一些更有意义的事情。这样的特性是强大的,但也是费神的,对于初次接触的同学而言,这里面的内容是有一定难度的。但慢慢细读下来,其实也是非常的简单,无外呼涉及到一点点简单的数学运算。但有一点要知道,你必须对CSS的自定义属性有所了解以及对CSS的属性值有深入的理解。

最后声明一点,这篇文章的思路来自于@Ana Tudor在CSS-Tricks上发表的教程,而且文章中有些图片也直接来源于教程中。这里特别感谢@Ana Tudor为我们学习CSS的自定义属性提供这么好的教程,更重要的是提供了一个全新的概念与技术原理。@Ana Tudor的文章中还提供了一些其他的示例,如果感兴趣的话,可以阅读她写的博客,难度会更大一些。再次感谢@Ana Tudor给我们提供这么好的教程。air max 90 essential metallic silver