如何通过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给我们提供这么好的教程。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章