不止于 CSS
14238
606
1523

今天,收到一个很有意思的提问,如何实现类似如下的背景效果图:

嗯?核心主体是由多个六边形网格叠加形成。

那么我们该如何实现它呢?使用纯 CSS 能够实现吗?

当然可以,下面我们就将尝试如何使用 CSS 去实现这样一个背景效果。

如何绘制六边形?

首先,看到这样一个图形,如果想要使用一个标签完成整个背景,最先想到的肯定是使用背景 background 实现,不过可惜的是,尽管 CSS 中的 background 非常之强大,但是没有特别好的方式让它足以批量生成重复的六边形背景。

因此,在这个需求中,我们可能不得不退而求其次,一个六边形实现使用一个标签完成。

那么,就拿 1 个 DIV 来说,我们有多少实现六边形的方式呢?这里简单介绍 2 种方式:

  • 使用 border 实现六边形
  • 使用 clip-path 实现六边形

使用 border 或者 clip-path 实现六边形

首先,使用 border 实现六边形。这里的核心在于上下两个三角形叠加中间一个矩形。这里,利用元素的两个伪元素实现上下两个三角形,从而让这个元素看起来像一个六边形。

思路比较简单,直接上代码:

.hexagon {
  position: relative;
  width: 200px;
  height: 100px;
  background-color: red;
}

.hexagon:before,
.hexagon:after {
  content: "";
  position: absolute;
  width: 0;
  height: 0;
  border-left: 100px solid transparent;
  border-right: 100px solid transparent;
}

.hexagon:before {
  bottom: 100%;
  border-bottom: 50px solid red;
}

.hexagon:after {
  top: 100%;
  border-top: 50px solid red;
}

上面的代码会创建一个宽度为 200 像素,高度为 100 像素的六边形,其中由两个三角形和一个矩形组成。使用伪元素的优点是可以很方便地控制六边形的大小、颜色等样式。

当然,上述的代码不是一个正六边形,这是因为正六边形中,元素的高是元素的宽的 1.1547 倍

并且,上述的方式也稍微复杂了点,因此,在今天,我们更推荐使用 clip-path 的方式去实现一个六边形:

.clippath {
    --w: 100px;
    width: var(--w);
    height: calc(var(--w) * 1.1547);
    clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
    background: deeppink;
    margin: auto;
}

这样,基于 clip-path,也能快速得到一个六边形图形:

CodePen Demo -- Two ways to achieve a hexagon

绘制多个六边形背景

好了,有了上一步的铺垫之后,接下来我们要做的,就是绘制多个六边形,组成背景。

但是我们仔细观察一下由多个六边形组成的背景,会发现每双数行的的六边形,需要向右侧有一个明显的缩进,宽度大概为单个六边形的宽度的一半:

这里其实是一个非常棘手的问题。首先,我们会想到这样一种解决方案:

  1. 每一行为一组,设置一个父 div 容器,填满六边形元素,设置元素不换行
  2. 给偶数行设置一个固定的 margin-left

基于这个策略,我们的代码,大概会是这样:

<div class="container">
    <div class="wrap">
    // ... 填满六边形
    </div>
    <div class="wrap" style="margin-left: 25px">
    // ... 填满六边形
    </div>
    <div class="wrap">
    // ... 填满六边形
    </div>
    <div class="wrap" style="margin-left: 25px">
    // ... 填满六边形
    </div>
</div>

可以看到,我们给偶数行,都添加了一个 margin-left

但是这个代码,会有几个问题:

  1. 我们的页面宽度不一定是固定的,那么每一行设置多少个子六边形元素比较合适呢?设置多了势必会带来浪费,少了又无法满足需求
  2. 多了一层嵌套,代码逻辑更为复杂

什么意思呢?也就是效果可能在屏幕非常宽的情况下,失效。

看看,正常情况,我们设置了每行 20 个六边形,下图是正常的

但是如果我们的屏幕特别宽,那么,可能会得到这样一种效果:

因此,这种方式存在非常大的弊端,我们希望能有一整布局方式,能够满足我们如下两个诉求:

  1. 所有六边形代码写在一个父容器下
  2. 这个弹性布局中,第二行的元素最左边,能够实现固定一个缩进

仔细思考一下,CSS 中有能够实现类似布局的方法么?

妙用 shape-outside 实现隔行错位布局

有的!在 CSS 中,有一个神奇的元素能够让元素以非直线形式排布。它就是 shape-outside

如果你对 shape-outside 不太了解,也可以先看看我的这篇文章 -- 奇妙的 CSS shapes

shape-outside 是 CSS 中的一个属性,用于控制元素的浮动方式。它允许你定义一个元素浮动时周围元素的形状。例如,你可以使用 shape-outside 属性来定义一个元素浮动时周围元素的形状为圆形、六边形等。

它和 clip-path 的语法非常类似,很容易触类旁通。看看实例,更易理解:

假设我们有下面这样的结构存在:

<div class="container">
    <div class="shape-outside">
      <img src="image.png">
    </div>
    xxxxxxxxxxx,文字描述,xxxxxxxxx
</div>

定义如下 CSS:

.shape-outside {
    width: 160px;
    height: 160px;
    shape-outside: circle(80px at 80px 80px);
    float: left;
}

注意,上面 .shape-outside 使用了浮动,并且定义了 shape-outside: circle(80px at 80px 80px) ,表示在元素的 (80px, 80px) 坐标处,生成一个 80px 半径的圆。

如此,将会产生一种图文混排的效果:

CodePen Demo -- 图文混排 shape-outside

总得来说,shape-outside 有两个核心特点:

  1. shape-outside 属性仅在元素定义了 float 属性且不为 none 时才会生效
  2. 它能够实现了文字根据图形的轮廓,在其周围排列

shape-outside 的本质

划重点,划重点,划重点。

所以,shape-outside 的本质其实是生成几何图形,并且裁剪掉其几何图形之外周围的区域,让内容能排列在这些被裁剪区域之内

所以,了解了这个本质之后,我们再将他运用在上面的六边形布局之中。

为了方便理解,我们首先使用文字代替上面的六边形,假设我们有这样一段文本内容:

<p>
Lorem ipsum dolor sit amet conse...
</p>
p {
    line-height: 36px;
    font-size: 24px;
}

非常平平无奇的一段代码,效果如下:

现在,我们想利用 shape-outside,让文本内容的偶数行,向内缩进 24px,怎么实现呢?非常简单:

p {
    position: relative;
    line-height: 36px;
    font-size: 24px;

    &::before {
        content: "";
        height: 100%;
        width: 24px;
        shape-outside: repeating-linear-gradient(
            transparent 0,
            transparent 36px,
            #000 36px,
            #000 72px
        );
        float: left;
    }
}

这样,我们就实现了文字隔行缩进 24px 的效果:

一定有小伙伴会很好奇,为什么呢?核心在于我们利用元素的伪元素实现了一个 shape-outside 图形,如果我们把这个图形用 background 绘制出来,其实它长这样:

p {
    position: relative;
    line-height: 36px;
    font-size: 24px;

    &::before {
        content: "";
        height: 100%;
        width: 24px;
        shape-outside: repeating-linear-gradient(
            transparent 0,
            transparent 36px,
            #000 36px,
            #000 72px
        );
        float: left;
        background: repeating-linear-gradient(
            transparent 0,
            transparent 36px,
            #f00 36px,
            #f00 72px
        );
    }
}

效果如下:

因为文本的行高是 36px,这样我们以 72 为一段,每 36px 绘制一段透明,另外 36px 绘制一段宽为 24px 的内容,这样,结合 shape-outside 的特性,我们就实现了隔行将内容向里面挤 24px 的效果!

非常的 Amazing 的技巧!完整的代码你可以戳这里:

CodePen Demo -- Shape-outside achieves even line indentation

基于这个技巧,我们就可以实现上述我们想要的效果了。我们回到正题,重新实现一个充满六边形的背景:

<ul class="wrap">
  <li></li>
  //... 非常多个 li
<ul>
:root {
  --s: 50px;  /* size  */
  --m: 4px;    /* margin */
  --perHeight: calc(calc(var(--s) * 2 * 1.1547) + calc(var(--m) * 4) - 0.4px)
}

.wrap {
    position: relative;
    height: 100%;
    font-size: 0;

    &::before {
        content: "";
        height: 100%;
        width: 27px;
        shape-outside: repeating-linear-gradient(
            transparent 0,
            transparent 70px,
            #000 70px,
            #000 var(--perHeight)
        );
        float: left;
    }
}

li {
    width: var(--s);
    height: calc(var(--s) * 1.1547); 
    background: #000;
    clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
    margin: var(--m);
    display: inline-block;
}

借助 shape-outside,我们就实现了隔行让我们的六边形向内缩进的诉求!效果如下:

当然,有一些优化点:

  1. 为了让两边不那么空,我们可以让整个容器更宽一点,譬如宽度为父元素的 120%,然后水平居中,这样,两侧的留白就解决了
  2. 让两行直接贴紧,可以设置一个 margin-bottom

做完这两点优化之后,效果如下:

可以做到任意屏幕宽度下的六边形完美平铺布局:

完整的代码你可以戳这里:CodePen Demo -- Hexagon Layout

配置上色彩变换

有了上述的铺垫后,要实现文章一开头的效果就不难了。

是的,我们要实现这样一个效果:

如何让它们动态的实现颜色变换呢?是给每一个六边形一个单独的颜色,然后进行动画吗?不,借助混合模式,我们可以非常快速的实现不同的颜色值。

首先,我们将上述效果,改成白底黑色六边形色块:

image

然后,利用父容器剩余的一个伪元素,我们叠加一层渐变层上去:

.wrap {
    position: relative;
    // 代码与上述保持一致

    &::before {
        content: "";
        // ... 实现 shape-outside 功能,代码与上述保持一致
    }
    
    &::after {
        content: "";
        position: absolute;
        inset: 0;
        background: linear-gradient(45deg, #f44336, #ff9800, #ffe607, #09d7c4, #1cbed3, #1d8ae2, #bc24d6);
    }
}

这样,我们就叠加了一层渐变色彩层在原本的六边形背景之上:

接着,只需要一个混合模式 mix-blend-mode: darken,就能实现六边形色块与上层渐变颜色的融合效果:

.wrap {
    position: relative;
    // 代码与上述保持一致

    &::before {
        content: "";
        // ... 实现 shape-outside 功能,代码与上述保持一致
    }
    
    &::after {
        content: "";
        position: absolute;
        inset: 0;
        background: linear-gradient(45deg, #f44336, #ff9800, #ffe607, #09d7c4, #1cbed3, #1d8ae2, #bc24d6);
        z-index: 1;
      + mix-blend-mode: darken;
    }
}

效果如下:

好, 我们再给上层的渐变色块,添加一个 filter: hue-rotate() 动画,实现色彩的渐变动画:

.wrap {
    position: relative;
    // 代码与上述保持一致

    &::before {
        content: "";
        // ... 实现 shape-outside 功能,代码与上述保持一致
    }
    
    &::after {
        content: "";
        position: absolute;
        inset: 0;
        background: linear-gradient(45deg, #f44336, #ff9800, #ffe607, #09d7c4, #1cbed3, #1d8ae2, #bc24d6);
        z-index: 1;
        mix-blend-mode: darken;
      + animation: change 10s infinite linear;
    }
}
@keyframes change {
    100% {
        filter: hue-rotate(360deg);
    }
}

这样,我们就完美的实现了我们想要的效果:

完整的代码,你可以戳这里:CodePen Demo -- Hexagon Gradient Layout

扩展延伸

当然,有了这个基础图形之后,其实我们可以基于这个图形,去做非常多有意思的效果。

下面我是尝试的一些效果示意,譬如,我们可以将颜色放置在六边形背景的下方,制作这样一种效果:

CodePen Demo -- Hexagon Gradient Layout

配合 mask 的蒙版效果及鼠标定位,我们还能实现这样一种有趣的交互效果:

CodePen Demo -- Hexagon Gradient & MASK Layout

当然,3D 效果也不在话下:

CodePen Demo -- 3D Hexagon Gradient Layout

最后

好了,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

今天逛博客网站 -- shoptalkshow,看到这样一个界面,非常有意思:

image

觉得它的风格很独特,尤其是其中一些边框。

嘿嘿,所以来一篇边框特辑,看看运用 CSS,可以在边框上整些什么花样。

border 属性

谈到边框,首先会想到 border,我们最常用的莫过于 soliddashed,上图中便出现了 dashed

除了最常见的 soliddashed,CSS border 还支持 nonehiddendotteddoublegrooveridgeinsetoutset 等样式。除去 nonehidden,看看所有原生支持的 border 的样式:

image

基础的就这些,如果希望实现一个其他样式的边框,或者给边框加上动画,那就需要配合一些其他属性,或是脑洞大开。OK,一起来看看一些额外的有意思的边框。

边框长度变化

先来个比较简单的,实现一个类似这样的边框效果:

border10

这里其实是借用了元素的两个伪元素。两个伪元素分别只设置上、左边框,下、右边框,通过 hover 时改变两个伪元素的高宽即可。非常好理解。

div {
    position: relative;
    border: 1px solid #03A9F3;
    
    &::before,
    &::after {
        content: "";
        position: absolute;
        width: 20px;
        height: 20px;
    }
    
    &::before {
        top: -5px;
        left: -5px;
        border-top: 1px solid var(--borderColor);
        border-left: 1px solid var(--borderColor);
    }
    
    &::after {
        right: -5px;
        bottom: -5px;
        border-bottom: 1px solid var(--borderColor);
        border-right: 1px solid var(--borderColor);
    }
    
    &:hover::before,
    &:hover::after {
        width: calc(100% + 9px);
        height: calc(100% + 9px);
    }
}

CodePen Demo -- width border animation

接下来,会开始加深一些难度。

虚线边框动画

使用 dashed 关键字,可以方便的创建虚线边框。

div {
    border: 1px dashed #333;
}

image

当然,我们的目的是让边框能够动起来。使用 dashed 关键字是没有办法的。但是实现虚线的方式在 CSS 中有很多种,譬如渐变就是一种很好的方式:

div {
    background: linear-gradient(90deg, #333 50%, transparent 0) repeat-x;
    background-size: 4px 1px;
    background-position: 0 0;
}

看看,使用渐变模拟的虚线如下:

image

好,渐变支持多重渐变,我们把容器的 4 个边都用渐变表示即可:

div {
    background: 
        linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
        linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
        linear-gradient(0deg, #333 50%, transparent 0) repeat-y,
        linear-gradient(0deg, #333 50%, transparent 0) repeat-y;
    background-size: 4px 1px, 4px 1px, 1px 4px, 1px 4px;
    background-position: 0 0, 0 100%, 0 0, 100% 0;
}

效果如下:

image

OK,至此,我们的虚线边框动画其实算是完成了一大半了。虽然 border-style: dashed 不支持动画,但是渐变支持呀。我们给上述 div 再加上一个 hover 效果,hover 的时候新增一个 animation 动画,改变元素的 background-position 即可。

div:hover {
    animation: linearGradientMove .3s infinite linear;
}

@keyframes linearGradientMove {
    100% {
        background-position: 4px 0, -4px 100%, 0 -4px, 100% 4px;
    }
}

OK,看看效果,hover 上去的时候,边框就能动起来,因为整个动画是首尾相连的,无限循环的动画看起来就像是虚线边框在一直运动,这算是一个小小的障眼法或者小技巧:

border2

这里还有另外一个小技巧,如果我们希望虚线边框动画是从其他边框,过渡到虚线边框,再行进动画。完全由渐变来模拟也是可以的,如果想节省一些代码,使用 border 会更快一些,譬如这样:

div {
    border: 1px solid #333;
    
    &:hover {
        border: none;
        background: 
            linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
            linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
            linear-gradient(0deg, #333 50%, transparent 0) repeat-y,
            linear-gradient(0deg, #333 50%, transparent 0) repeat-y;
        background-size: 4px 1px, 4px 1px, 1px 4px, 1px 4px;
        background-position: 0 0, 0 100%, 0 0, 100% 0;
    }
}

由于 border 和 background 在盒子模型上位置的差异,视觉上会有一个很明显的错位的感觉:

border3

要想解决这个问题,我们可以把 border 替换成 outline,因为 outline 可以设置 outline-offset。便能完美解决这个问题:

div {
    outline: 1px solid #333;
    outline-offset: -1px;
    
    &:hover {
        outline: none;
    }
}

最后看看运用到实际按钮上的效果:

border14

上述 Demo 完整代码如下:

CodePen Demo -- dashed border animation

其实由于背景和边框的特殊关系,使用 border 的时候,通过修改 background-position 也是可以解决的,就是比较绕。关于背景和边框的填充关系,可以看这篇文章:条纹边框的多种实现方式

渐变的其他妙用

利用渐变,不仅仅只是能完成上述的效果。

我们继续深挖渐变,利用渐变实现这样一个背景:

div {
    position: relative;
    
    &::after {
        content: '';
        position: absolute;
        left: -50%;
        top: -50%;
        width: 200%;
        height: 200%;
        background-repeat: no-repeat;
        background-size: 50% 50%, 50% 50%;
        background-position: 0 0, 100% 0, 100% 100%, 0 100%;
        background-image: linear-gradient(#399953, #399953), linear-gradient(#fbb300, #fbb300), linear-gradient(#d53e33, #d53e33), linear-gradient(#377af5, #377af5);
    }
}

注意,这里运用了元素的伪元素生成的这个图形,并且,宽高都是父元素的 200%,超出则 overflow: hidden

image

接下来,给它加上旋转:

div {
    animation: rotate 4s linear infinite;
}

@keyframes rotate {
	100% {
		transform: rotate(1turn);
	}
}

看看效果:

border4

最后,再利用一个伪元素,将中间遮罩起来,一个 Nice 的边框动画就出来了:

border5

上述 Demo 完整代码如下,这个效果我最早见于这位作者 -- Jesse B

CodePen Demo -- gradient border animation

改变渐变的颜色

掌握了上述的基本技巧之后,我们可以再对渐变的颜色做一些调整,我们将 4 种颜色变成 1 种颜色:

div::after {
    content: '';
    position: absolute;
    left: -50%;
    top: -50%;
    width: 200%;
    height: 200%;
    background-color: #fff;
    background-repeat: no-repeat;
    background-size: 50% 50%;
    background-position: 0 0;
    background-image: linear-gradient(#399953, #399953);
}

得到这样一个图形:

image

同样的,让它旋转一起,一个单色追逐的边框动画就出来了:

border11

CodePen Demo -- gradient border animation 2

Wow,很不错的样子。不过如果是单线条,有个很明显的缺陷,就是边框的末尾是一个小三角而不是垂直的,可能有些场景不适用或者 PM 接受不了。

image

那有没有什么办法能够消除掉这些小三角呢?有的,在下文中我们再介绍一种方法,利用 clip-path ,消除掉这些小三角。

conic-gradient 的妙用

再介绍 clip-path 之前,先讲讲角向渐变。

上述主要用到了的是线性渐变 linear-gradient 。我们使用角向渐变 conic-gradient 其实完全也可以实现一模一样的效果。

我们试着使用 conic-gradient 也实现一次,这次换一种暗黑风格。核心代码如下:

.conic {
	position: relative;
	
	&::before {
		content: '';
		position: absolute;
		left: -50%;
		top: -50%;
		width: 200%;
		height: 200%;
		background: conic-gradient(transparent, rgba(168, 239, 255, 1), transparent 30%);
		animation: rotate 4s linear infinite;
	}
}
@keyframes rotate {
	100% {
		transform: rotate(1turn);
	}
}

效果图和示意图如下,旋转一个部分角向渐变的图形,中间的部分使用另外一个伪元素进行遮罩,只漏出线条部分即可:

border13

CodePen Demo -- Rotating border 3

clip-path 的妙用

又是老朋友 clip-path,有意思的事情它总不会缺席。

clip-path 本身是可以进行坐标点的动画的,从一个裁剪形状变换到另外一个裁剪形状。

利用这个特点,我们可以巧妙的实现这样一种 border 跟随效果。伪代码如下:

div {
    position: relative;

    &::before {
        content: "";
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        border: 2px solid gold;
        animation: clippath 3s infinite linear;
    }
}

@keyframes clippath {
    0%,
    100% {
        clip-path: inset(0 0 95% 0);
    }
    25% {
        clip-path: inset(0 95% 0 0);
    }
    50% {
        clip-path: inset(95% 0 0 0);
    }
    75% {
        clip-path: inset(0 0 0 95%);
    }
}

效果图与示意图一起:

border8

CodePen - clip-path border animation

这里,因为会裁剪元素,借用伪元素作为背景进行裁剪并动画即可,使用 clip-path 的优点是,切割出来的边框不会产生小三角。同时,这种方法,也是支持圆角 border-radius 的。

如果我们把另外一个伪元素也用上,实际实现一个按钮样式,可以得到这样的效果:

border12

CodePen - clip-path border animation 2

overflow 的妙用

下面这个技巧利用 overflow 实现。实现这样一个边框动画:

border6

为何说是利用 overflow 实现?

贴个示意图:

border7

CodePen Demo -- 巧用overflow及transform实现线条hover效果

两个核心点:

  1. 我们利用 overflow: hidden,把原本在容器外的一整个元素隐藏了起来
  2. 利用了 transform-origin,控制了元素的旋转中心

发现没,其实几乎大部分的有意思的 CSS 效果,都是运用了类似技巧:

简单的说就是,我们看到的动画只是原本现象的一小部分,通过特定的裁剪、透明度的变化、遮罩等,让我们最后只看到了原本现象的一部分。

border-radius 边框变化

利用 border-radius 的变化,也能制作出有意思的效果。这个技巧,通常会用在一些人物头像上。

div {
    border: 1px solid #03A9F3;
    border-radius: 30% 60% 70% 40% / 50% 60% 30% 60%;
    transition: all .5s;
}

.border-radius:hover {
    border-radius: 48% 28% 38% 49%/ 38% 42% 36% 47%;
}

CodePen Demo -- border-radius border animation

border-image 的妙用

利用 border-image,我们也可以实现一些有意思的边框动画。关于 border-image,有一篇非常好的讲解文章 -- border-image 的正确用法,本文不对基本定义做过多的讲解。

如果我们有这样一张图:

image

便可以利用 border-image-sliceborder-image-repeat 的特性,得到类似的边框图案:

div {
  width: 200px;
  height: 120px;
  border: 24px solid;
  border-image: url(image-url);
  border-image-slice: 32;
  border-image-repeat: round;
}

在这个基础上,可以随便改变元素的高宽,如此便能扩展到任意大小的容器边框中:

image

CodePen Demo -- border-image Demo

接着,在这篇文章 -- How to Animate a SVG with border-image 中,还讲解了一种利用 border-image 的边框动画,非常的酷炫。

与上面例子不一样的是,我们只需要让我们的图案,动起来:

那么,我们也就能得到运动的边框图,代码完全一样,但是,边框是运动的:

border9

CodePen Demo -- Dancing Skull Border

border-image 使用渐变

border-image 除了贴图引用 url 之外,也是可以直接填充颜色或者是渐变的。

之前也有一篇关于 border-image 的文章 -- 巧妙实现带圆角的渐变边框

我们可以利用 border-image + filter + clip-path 实现渐变变换的圆角边框:

.border-image-clip-path {
    width: 200px;
    height: 100px;
    border: 10px solid;
    border-image: linear-gradient(45deg, gold, deeppink) 1;
    clip-path: inset(0px round 10px);
    animation: huerotate 6s infinite linear;
    filter: hue-rotate(360deg);
}

@keyframes huerotate {
    0% {
        filter: hue-rotate(0deg);
    }
    100% {
        filter: hue-rotate(360deg);
    }
}

clipboder

CodePen Demo -- clip-path、border-image 加 filter 实现圆角渐变边框

最后

本文介绍了一些我认为比较有意思的边框动画小技巧,当然 CSS 产生还有非常多有意思的效果,限于篇幅,不一一展开。

本文到此结束,希望对你有帮助 :),想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

3、层叠顺序(stacking level)与堆栈上下文(stacking context)知多少?

z-index 看上去其实很简单,根据 z-index 的高低决定层叠的优先级,实则深入进去,会发现内有乾坤。

看看下面这题,定义两个 div A 和 B,被包括在同一个父 div 标签下。HTML结构如下:

<div class="container">
    <div class="inline-block">#divA display:inline-block</div>
    <div class="float"> #divB float:left</div>
</div>

它们的 CSS 定义如下:

.container{
    position:relative;
    background:#ddd;
}
.container > div{
    width:200px;
    height:200px;
}
.float{
    float:left;
    background-color:deeppink;
}
.inline-block{
    display:inline-block;
    background-color:yellowgreen;
    margin-left:-100px;
}

大概描述起来,意思就是拥有共同父容器的两个 DIV 重叠在一起,是 display:inline-block 叠在上面,还是 float:left 叠在上面?

注意这里 DOM 的顺序,是先生成 display:inline-block ,再生成 float:left 。当然也可以把两个的 DOM 顺序调转如下:

<div class="container">
    <div class="float"> #divB float:left</div>
    <div class="inline-block">#divA display:inline-block</div>
</div>

会发现,无论顺序如何,始终是 display:inline-blockdiv 叠在上方。

Demo戳我

这里其实是涉及了所谓的层叠水平(stacking level),有一张图可以很好的诠释:

运用上图的逻辑,上面的题目就迎刃而解,inline-blcokstacking level 比之 float 要高,所以无论 DOM 的先后顺序都堆叠在上面。

不过上面图示的说法有一些不准确,按照 W3官方 的说法,准确的 7 层为:

1、the background and borders of the element forming the stacking context.

2、the child stacking contexts with negative stack levels (most negative first).

3、the in-flow, non-inline-level, non-positioned descendants.

4、the non-positioned floats.

5、the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.

6、the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.

7、the child stacking contexts with positive stack levels (least positive first).

稍微翻译一下:

1、形成堆叠上下文环境的元素的背景与边框

2、拥有负 z-index 的子堆叠上下文元素 (负的越高越堆叠层级越低)

3、正常流式布局,非 inline-block,无 position 定位(static除外)的子元素

4、无 position 定位(static除外)的 float 浮动元素

5、正常流式布局, inline-block元素,无 position 定位(static除外)的子元素(包括 display:table 和 display:inline )

6、拥有 z-index:0 的子堆叠上下文元素

7、拥有正 z-index: 的子堆叠上下文元素(正的越低越堆叠层级越低)

所以我们的两个 div 的比较是基于上面所列出来的 4 和 5 。5 的 stacking level 更高,所以叠得更高。

不过!不过!不过!重点来了,请注意,上面的比较是基于两个 div 都没有形成 堆叠上下文 这个为基础的。下面我们修改一下题目,给两个 div ,增加一个 opacity:

.container{
    position:relative;
    background:#ddd;
}
.container > div{
    width:200px;
    height:200px;
    opacity:0.9; // 注意这里,增加一个 opacity
}
.float{
    float:left;
    background-color:deeppink;
}
.inline-block{
    display:inline-block;
    background-color:yellowgreen;
    margin-left:-100px;
}

Demo戳我

会看到,inline-blockdiv 不再一定叠在 floatdiv 之上,而是和 HTML 代码中 DOM 的堆放顺序有关,后添加的 div 会 叠在先添加的 div 之上。

这里的关键点在于,添加的 opacity:0.9 这个让两个 div 都生成了 stacking context(堆叠上下文) 的概念。此时,要对两者进行层叠排列,就需要 z-index ,z-index 越高的层叠层级越高。

堆叠上下文是HTML元素的三维概念,这些HTML元素在一条假想的相对于面向(电脑屏幕的)视窗或者网页的用户的 z 轴上延伸,HTML 元素依据其自身属性按照优先级顺序占用层叠上下文的空间。

那么,如何触发一个元素形成 堆叠上下文 ?方法如下,摘自 MDN

  • 根元素 (HTML),
  • z-index 值不为 "auto"的 绝对/相对定位,
  • 一个 z-index 值不为 "auto"的 flex 项目 (flex item),即:父元素 display: flex|inline-flex,
  • opacity 属性值小于 1 的元素(参考 the specification for opacity),
  • transform 属性值不为 "none"的元素,
  • mix-blend-mode 属性值不为 "normal"的元素,
  • filter值不为“none”的元素,
  • perspective值不为“none”的元素,
  • isolation 属性被设置为 "isolate"的元素,
  • position: fixed
  • 在 will-change 中指定了任意 CSS 属性,即便你没有直接指定这些属性的值
  • -webkit-overflow-scrolling 属性被设置 "touch"的元素

所以,上面我们给两个 div 添加 opacity 属性的目的就是为了形成 stacking context。也就是说添加 opacity 替换成上面列出来这些属性都是可以达到同样的效果。

在层叠上下文中,其子元素同样也按照上面解释的规则进行层叠。 特别值得一提的是,其子元素的 z-index 值只在父级层叠上下文中有意义。意思就是父元素的 z-index 低于父元素另一个同级元素,子元素 z-index 再高也没用。

理解上面的 stacking-levelstacking-context 是理解 CSS 的层叠顺序的关键。


最后,新开通的公众号求关注,形式希望是更短的篇幅,质量更高一些的技巧类文章,包括但不局限于 CSS:

image

终于,在漫长的等待下,容器查询(CSS Container Queries)将在 Chrome 105 版本得到正式的支持!

image

而目前,我们也能在 Chrome Canary 版本中,或者在 Chrome 93~104 通过开启 Enable CSS Container Queries 特性抢先体验。

响应式过往的痛点

在之前,响应式有这么个掣肘。同一 DOM 的不同布局形态如果想要变化,需要依赖诸如媒体查询来实现。

像是这样:

通过浏览器视窗大小的变化,借助媒体查询,实现不一样的布局。

但是,在现如今,大部分 PC 端页面使用的是基于 Flex/Grid 的弹性布局。

很多时候,当内容数不确定的时候,即便是相同的浏览器视窗宽度下,元素的布局及宽度可能也是不一致的。

考虑下面这种情况:

<!-- 情况一  -->
<ul class="wrap">
    <li></li>
    <li></li>
    <li></li>
</ul>
<!-- 情况二  -->
<ul class="wrap">
    <li></li>
    <li></li>
    <li></li>
    <li></li>
</ul>
.wrap {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}
li {
    width: 190px;
    height: 100px;
    flex-grow: 1;
    flex-shrink: 0;
}

这种情况下,如果需要在不同宽度下对最后一个元素做一下处理,传统方式还是比较麻烦的。

在这种情况下,容器查询(CSS Container Queries)就应运而生了!

容器查询的能力

容器查询它给予了 CSS,在不改变浏览器视口宽度的前提下,只是根据容器的宽度变化,对布局做成调整的能力。

还是上面的例子,简单的代码示意:

<div class="wrap">
    <div class="g-container">
        <div class="child">Title</div>
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus vel eligendi, esse illum similique sint!!</p>
    </div>
</div>
.wrap {
    width: 500px;
    resize: horizontal;
    overflow: auto;
}
.g-container {
    display: flex;
    flex-wrap: nowrap;
}
.wrap {
    /* CSS CONTAINER */
    container-name: wrap;
    container-type: inline-size;
}
@container wrap (max-width: 400px) {
    .g-container {
        flex-wrap: wrap;
        flex-direction: column;
    }
}

像是这样,我们通过 resize: horizontal 来模拟单个容器的宽度变化,在这种情况下,容器查询能够做到在不同宽度下,改变容器内部的布局。

这样,就简单实现了一个容器查询功能:

注意,仔细和上面的例子作对比,这里,浏览器的视口宽度是没有变化的,变化的只是容器的宽度!

媒体查询与容器查询的异同,通过一张简单的图看看,核心的点在于容器的宽度发生变化时,视口的宽度不一定会发生变化:

我们简单拆解下上述的代码,非常好理解。

  1. .warp 的样式中,通过 container-name: wrap 注册一个容器
  2. 注册完容器之后,便可以通过 @container wrap () 容器查询语法,在内部写入不同情况下的另外一套样式
  3. 这里 @container wrap (max-width: 400px) {} 的意思便是,当 .wrap 容器的宽度小于 400 px 时,采用内部定义的样式,否则,使用外部默认的样式

关于容器查询更为具体的语法,我建议还是上 MDN 或者规范详细看看 -- MDN -- CSS Container Queries

关于容器查询的一些思考

在第一次看到这个语法之后,我最先想到的场景便是字体的自适应大小。

我们来看这样一个场景,很多时候,我们无法预估文案内容的多少。因此,会希望当内容较多时,字体较小,而当内容不足一行或者非常少的时候,字体较大:

CodePen Demo -- Container Quries Demo

当然,现阶段我暂时没有试出来在容器查询中,容器的宽度能够随着输入的变化动态改变容器大小,这里目前有点瑕疵,是个需要继续钻研的点。

当然,在那些能够事先知道不同宽度,预设不同布局的场景下,容器查询的用武之地是非常之大的。

我们可以利用它快速构建在容器不同宽度下的不同表现

譬如这样一个 DEMO:

CodePen Demo -- CSS Container Queries

总得来说,容器查询,还是处于比较早期的发展之中,许多有意思的用法还有待挖掘。但它确实算得上是 CSS 今年比较大的一个革新。

最后

好了,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

在过往,我们想要实现一个图片的渐隐消失。最常见的莫过于整体透明度的变化,像是这样:

<div class="img"></div>
div {
    width: 300px;
    height: 300px;
    background: url(image.jpg);
    transition: .4s;
}
.img:hover {
    opacity: 0;
}

但是,CSS 的功能如此强大的今天。我们可以利用 CSS 实现的渐隐效果已经不再是如此的简单。

想想看,下面这样一个效果,是 CSS 能够实现的么?

答案是肯定的!本文就将一步一步,从零开始,仅仅使用一个标签,实现上述的图片渐隐效果。

这里,有两个核心的点:

  1. 如何将一张图片切割的这么细,切割成这么多块?
  2. 基于上述 (1)的基础上,又该如何分别控制这些小块的独立隐藏和展示呢?

莫慌,让我们一步一步来解决他们。

强大的 Mask

首先,我们需要用到 Mask。

在 CSS 中,mask 属性允许使用者通过遮罩或者裁切特定区域的图片的方式来隐藏一个元素的部分或者全部可见区域。

语法

最基本,使用 mask 的方式是借助图片,类似这样:

{
    /* Image values */
    mask: url(mask.png);                       /* 使用位图来做遮罩 */
    mask: url(masks.svg#star);                 /* 使用 SVG 图形中的形状来做遮罩 */
}

当然,使用图片的方式后文会再讲。借助图片的方式其实比较繁琐,因为我们首先还得准备相应的图片素材,除了图片,mask 还可以接受一个类似 background 的参数,也就是渐变。

类似如下使用方法:

{
    mask: linear-gradient(#000, transparent)                      /* 使用渐变来做遮罩 */
}

那该具体怎么使用呢?一个非常简单的例子,上述我们创造了一个从黑色到透明渐变色,我们将它运用到实际中,代码类似这样:

下面这样一张图片,叠加上一个从透明到黑色的渐变,

{
    background: url(image.png) ;
    mask: linear-gradient(90deg, transparent, #fff);
}

应用了 mask 之后,就会变成这样:

image

这个 DEMO,可以先简单了解到 mask 的基本用法。

这里得到了使用 mask 最重要结论:图片与 mask 生成的渐变的 transparent 的重叠部分,将会变得透明。

值得注意的是,上面的渐变使用的是 linear-gradient(90deg, transparent, #fff),这里的 #fff 纯色部分其实换成任意颜色都可以,不影响效果。

CodePen Demo -- 使用 MASK 的基本使用

强大的 CSS @Property

CSS @Property,大家应该不那么陌生了。

@Property CSS at-rule 是 CSS Houdini API 的一部分, 它允许开发者显式地定义他们的 CSS 自定义属性,允许进行属性类型检查、设定默认值以及定义该自定义属性是否可以被继承。

如果你对 CSS @Property 还有所疑惑,建议你先快速读一读这篇文章 -- CSS @property,让不可能变可能

回到我们的正题,如果我们想给上述使用 Mask 的代码,添加上动画,我们期望代码大概是这样:

div {
    width: 300px;
    height: 300px;
    background: url(image.jpg);
    mask: linear-gradient(rgba(0, 0, 0, 1), rgba(0, 0, 0, 1));
}
.img:hover {
    mask: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0));
}

这里,mask 的是从 mask: linear-gradient(rgba(0, 0, 0, 1), rgba(0, 0, 0, 1))mask: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)) 变化的。

但是实际上,这样并不会产生任何的动画效果。

原因在于,我们 Mask 属性本身是不支持过渡动画的!

但是,利用上 CSS @Property,整个效果就不一样了。借助,CSS @Property,我们改造一下代码:

@property --m-0 {
   syntax: "<number>";
   initial-value: 1;
   inherits: false;
}
div {
    width: 300px;
    height: 300px;
    background: url(image.jpg);
    mask: linear-gradient(90deg, rgba(0, 0, 0, var(--m-0)), rgba(0, 0, 0, var(--m-0)));
    transition: --m-0 0.5s;
}
div:hover {
    --m-0: 0;
}

我们利用 CSS @Property 定义了一个名为 --m-0 的变量,然后,我们将整个动画过渡效果赋予了这个变量,而不是整个 mask。

利用这个小技巧,我们就可以成功的实现基于 mask 属性的动画效果:

借助多重 mask 分割图片

到了这一步,后面的步骤其实就很明朗了。

由于 mask 拥有和 background 一样的特性。因此,mask 是可以有多重 mask 的。也就是说,我们可以设置多个不同的 mask 效果给同一个元素。

什么意思呢?上面的效果只有一重 mask,我们稍微添加一些 mask 代码,让它变成 2 重 mask:

@property --m-0 {
   syntax: "<number>";
   initial-value: 1;
   inherits: false;
}
@property --m-1 {
   syntax: "<number>";
   initial-value: 1;
   inherits: false;
}
div {
    mask: 
        linear-gradient(90deg, rgba(0, 0, 0, var(--m-0)), rgba(0, 0, 0, var(--m-0))),
        linear-gradient(90deg, rgba(0, 0, 0, var(--m-1)), rgba(0, 0, 0, var(--m-1)));
    mask-size: 50% 100%;
    mask-position: left, right;
    mask-repeat: no-repeat;
    transition: 
        --m-0 0.3s,
        --m-1 0.25s 0.15s;
}
div:hover {
    --m-0: 0;
    --m-1: 0;
}

这样,我们的步骤大概是:

  1. 首先将 mask 一分为二,左右两边各一个
  2. 然后,设置了两个基于 CSS @Property 的变量,--m-0--m-0
  3. 然后,给它们设置了不同的过渡时间和过渡延迟时间
  4. 在 hover 的一瞬间,再将这两个变量的值,都置为 0,也就是实现 linear-gradient(90deg, rgba(0, 0, 0, 1), rgba(0, 0, 0, 1))linear-gradient(90deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)) 的变化,用于隐藏对应 mask 块
  5. 由于设置了不同的过渡时间和延迟时间,整体上看上去,整个动画就分成了两部分

看看效果:

继续切割为 4 重 mask

好,既然 2 重 mask 效果没问题,那么我们可以再进一步,将整个效果切割为 4 个 mask。代码还是如法炮制,这里我再贴上核心代码:

@property --m-0 {
   syntax: "<number>";
   initial-value: 1;
   inherits: false;
}
@property --m-1 {
   syntax: "<number>";
   initial-value: 1;
   inherits: false;
}
@property --m-2 {
   syntax: "<number>";
   initial-value: 1;
   inherits: false;
}
@property --m-3 {
   syntax: "<number>";
   initial-value: 1;
   inherits: false;
}
div {
    mask: 
        linear-gradient(90deg, rgba(0, 0, 0, var(--m-0)), rgba(0, 0, 0, var(--m-0))),
        linear-gradient(90deg, rgba(0, 0, 0, var(--m-1)), rgba(0, 0, 0, var(--m-1))),
        linear-gradient(90deg, rgba(0, 0, 0, var(--m-2)), rgba(0, 0, 0, var(--m-2))),
        linear-gradient(90deg, rgba(0, 0, 0, var(--m-3)), rgba(0, 0, 0, var(--m-3)));
    mask-size: 50% 50%;
    mask-repeat: no-repeat;
    mask-position: left top, right top, left bottom, bottom right;
    transition: 
        --m-0 0.3s,
        --m-1 0.15s 0.1s,
        --m-2 0.25s 0.21s,
        --m-3 0.19s 0.15s;
}
div:hover {
    --m-0: 0;
    --m-1: 0;
    --m-2: 0;
    --m-3: 0;
}

这样,我们就可以得到 4 块分割图片的 mask 消失效果:

好,再依次类推,我们就可以得到分割为 9 块的,分割为 16 块的。由于代码太多,就简单看看效果:

CodePen Demo -- 基于 @property 和 mask 的图片渐隐消失术

基于 SCSS 简化代码

那么,如果我们要分割为 100 块呢?或者 400 块呢?还要手写这些代码吗?

当然不需要,由于上面的代码的规律非常的明显,我们可以借助预处理器很好的封装整个效果。从而快速的实现切割成任意规则块数的效果。

完整的代码如下:

$count: 400;
$sqrt: 20;
$per: 100% / $sqrt;
$width: 300px;
$perWid: 15;

@for $i from 1 to ($count + 1) {
    @property --m-#{$i} {
       syntax: "<number>";
       initial-value: 1;
       inherits: false;
    }
}
@function bgSet($n) {
    $bg : radial-gradient(rgba(0, 0, 0, var(--m-1)), rgba(0, 0, 0, var(--m-1)));
    
    @for $i from 2 through $n {         
        $bg: $bg, radial-gradient(rgba(0, 0, 0, var(--m-#{$i})), rgba(0, 0, 0, var(--m-#{$i})));
    }
    
    @return $bg;
}
@function positionSet($n) {
    $bgPosition: ();

    @for $i from 0 through ($n) {   
        @for $j from 0 through ($n - 1) {  
            $bgPosition: $bgPosition, #{$i * $perWid}px #{$j * $perWid}px;
        }
    }
    
    @return $bgPosition;
}
@function transitionSet($n) {
    $transition: --m-1 0.1s 0.1s;

    @for $i from 1 through $n {   
        $transition: $transition, --m-#{$i} #{random(500)}ms #{random(500)}ms;
    }
    
    @return $transition;
}
div {
    width: $width;
    height: $width;
    background: url(image.jpg);
    mask: bgSet($count);
    mask-size: $per $per;
    mask-repeat: no-repeat;
    mask-position: positionSet($sqrt); 
    transition: transitionSet($count);
}
div:hover {
    @for $i from 1 through $count {         
        --m-#{$i}: 0;
    }
}

这里,简单解释一下,以生成 400 块小块为例子:

  1. 最上面的 SCSS 变量定义中,
    • $count 是我们最终生成的块数
    • $sqrt 是每行以及每列会拥有的块数
    • $per 是每一块占整体图片元素的百分比值
    • $width 是整个图片的宽高值
    • $perWid 是每一块的宽高值
  2. 利用了最上面的一段循环函数,批量的生成 CSS @Property 变量,从 --m-0--m-400
  3. @function bgSet($n) {} 是生成 400 块 mask 片段
  4. @function positionSet($n) 是生成 400 块 mask 的 mask-position,也就是生成 400 段不同定位,让 400 块 mask 刚好覆盖整个图片
  5. @function transitionSet($n) {} 是随机设置每个块的动画时间和延迟时间
  6. 代码最下面,还有一段循环函数,生成 400 个 CSS @Property 变量的 hover 值,当 hover 的时候,全部变成 0

这样,我们就实现了 400 分块的渐隐效果。效果如下:

CodePen Demo -- 基于 @property 和 mask 的图片渐隐消失术

调整过渡变量,控制方向

当然,上面我们的对每一个小块的 transition 的过渡时间和过渡延迟时间的设置,都是随机的:

@function transitionSet($n) {
    $transition: --m-1 0.1s 0.1s;

    @for $i from 1 through $n {   
        $transition: $transition, --m-#{$i} #{random(500)}ms #{random(500)}ms;
    }
    
    @return $transition;
}

我们完全可以通过一定的控制,让过渡效果不那么随机,譬如有一定的方向感。

下面,我们通过让动画的延迟时间与 $i,也就是 mask 小块的 index 挂钩:

@function transitionSet($n) {
    $transition: --m-1 0.1s 0.1s;

    @for $i from 1 through $n {   
        $transition: $transition, --m-#{$i} #{100 + random(500)}ms #{($i / 50) * random(100)}ms;
    }
    
    @return $transition;
}

那么,整个动画的方向就是从左往右逐渐消失:

CodePen Demo -- 基于 @property 和 mask 的图片渐隐消失术 2

当然,有意思的是,这个效果,不仅仅能够运用在图片上,它其实可以作用在任何元素之上!

譬如,我们有的只是一段纯文本,同样适用这个效果:

CodePen Demo -- 基于 @property 和 mask 的文本渐隐消失术

总结

到这里,简单总结一下。本文,我们核心利用了 CSS @propery 和 mask,实现了一些原本看上去需要非常多 div 才能实现或者是需要借助 Canvas 才能实现的效果。同时,我们借助了 SCSS 预处理器,在寻找到规律后,极大的简化了 CSS 代码的书写量。

到今天,强大的 CSS 已经允许我们去做越来越多更有意思的动效,CSS @propery 和 mask 这两个属性在现代 CSS 发挥了非常重要的作用,非常建议大家认真掌握以下这两个属性。

最后

好了,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

CSS 属性选择器,可以通过已经存在的属性名或属性值匹配元素。

属性选择器是在 CSS2 中引入的并且在 CSS3 中得到了很好拓展。本文将会比较全面的介绍属性选择器,尽可能的去挖掘这个选择器在不同场景下的不同用法。

简单的语法介绍

  • [attr]:该选择器选择包含 attr 属性的所有元素,不论 attr 的值为何。
  • [attr=val]:该选择器仅选择 attr 属性被赋值为 val 的所有元素。
  • [attr~=val]:该选择器仅选择具有 attr 属性的元素,而且要求 val 值是 attr 值包含的被空格分隔的取值列表里中的一个。

子串值(Substring value)属性选择器,

下面几个属于 CSS3 新增语法,也被称为“伪正则选择器”,因为它们提供类似 regular expression 的灵活匹配方式。

  • [attr|=val] : 选择attr属性的值是 val 或值以 val- 开头的元素(注意,这里的 “-” 不是一个错误,这是用来处理语言编码的)。
  • [attr^=val] : 选择attr属性的值以 val 开头(包括 val)的元素。
  • [attr$=val] : 选择attr属性的值以 val 结尾(包括 val)的元素。
  • [attr*=val] : 选择attr属性的值中包含子字符串 val 的元素(一个子字符串就是一个字符串的一部分而已,例如,”cat“ 是 字符串 ”caterpillar“ 的子字符串

CSS 属性选择器的最基本用法

属性选择器最基本的用法,就是通过元素的属性值去选择 DOM 元素。像这样,将选中所有带 href 属性的DOM 元素:

[href] {
    color: red;
}

CodePen Demo -- 属性选择器基本用法

复杂一点点的用法

层叠选择

div [href]{
...
}

多条件复合选择

选择一个 img 标签,它含有 title 属性,并且包含类名为 logo 的元素。

img[title][class~=logo]{
...
}

伪正则写法

  • i 参数

忽略类名的大小写限制,选择包含 class 类名包含子字符串为 text,Text,TeXt... 等情况的 p 元素。
这里的 i 的含义就是正则里面参数 i 的含义,ignore,忽略大小写的意思。

p[class*="text" i] {
...
}

所以上面的选择器可以选中类似这样的目标元素:

<p class="text"></p>
<p class="nameText"></p>
<p class="desc textarea"></p>
  • g 参数

与正则表达式不一样,参数 g 在这里表示大小写敏感(case-sensitively)。然而,这个属性当前仍未纳入标准,支持的浏览器不多。

CodePen Demo -- 属性选择器的伪正则用法

配合 :not() 伪类

还有一种比较常用的场景就是搭配:not() 伪类,完成一些判断检测性的功能。譬如下面这个选择器,就可以选取所有没有 [href] 属性的 a 标签,添加一个红色边框。

a:not([href]){
    border: 1px solid red;
}

当然,复杂一点,我们可以搭配不仅仅一个 :not()伪类,像是这样,可以同时多个配合使用,选择一个 href, target, rel 属性都没有的 a 标签:

a:not([href]):not([target]):not([rel]){
    border: 1px solid blue;
}

CodePen Demo -- 属性选择器配合 :not 伪类

重写行内样式?

甚至乎,如果有这种场景,我们还可以覆盖掉行内样式,像这样:

<p style="height: 24px; color: red;">xxxxxx</p>

我们可以使用属性选择器强制覆盖掉上述样式:

[style*="color: red"] {
    color: blue !important;
}

组合拳用法,搭配伪元素提升用户体验

当然,属性选择器不一定只是单单的进行标签的选择。

配合上伪元素,我们可以实现很多有助提升用户体验的功能。

角标功能

这里有一个小知识点,伪元素的 content 属性,通过 attr(xxx),可以读取到对应 DOM 元素标签名为 xxx 的属性的值。

所以,配合属性选择器,我们可以很容易的实现一些角标功能:

<div count=“5“>Message</div>
div {
    position: relative;
    width: 200px;
    height: 64px;
}

div::before {
    content: attr(count);
    ...
}

image

这里右上角的数字 5 提示角标,就是使用属性选择器配合伪元素实现,可以适应各种长度,以及中英文,能够节省一些标签。CodePen Demo -- 属性选择器实现角标功能

属性选择器配合伪元素实现类 title 功能

我们都知道,如果给一个图片添加一个 title 属性,当 hover 到图片上面的时,会展示 title 属性里面附加的内容,类似这样:

<img src="xxxxxxxxx" title="风景图片">

attributeselector

这里不一定是 img 标签,其他标签添加 title 属性都能有类似的效果。但是这里会有两个问题:

  • 响应太慢,通常鼠标 hover 上去要隔 1s 左右才会出现这个 title 框
  • 框体结构无法自定义,弹出框的样式无法自定义

所以这里,如果我们希望有一些自己能够控制样式的可快速响应的浮层,可以自定义一个类 title 属性,我们把它称作 popTitle,那么可以这样操作:

<p class="title" popTitle="文字弹出">这是一段描述性文字</p>
<p class="title" popTitle="标题A">这是一段描述性文字</p>
p[popTitle]:hover::before {
    content: attr(popTitle);
    position: absolute;
    color: red;
    border: 1px solid #000;
    ...
}

对比一下,第一个是原生自带的 title 属性,下面两个是使用属性选择器配合伪元素模拟的提示:

attributeselector2

浏览器自带的 title 属性延迟响应是添加一层防抖保护,避免频繁触发,这里也可以通过对伪元素添加一个100毫秒级的 transition-delay 实现延迟展示。

CodePen Demo -- 属性选择器配合伪元素实现类 title 功能

商品展示提示效果

好,上面的运用实例我们再拓展一下,考虑如何更好的运用到实际业务中,其实也是有很多用武之地的。譬如说,通过属性选择器给图片添加标签,类似一些电商网站会用到的一个效果。

我们希望给图片添加一些标签,在 hover 图片的时候展示出来。

当然,CSS 中,诸如 <img><input><iframe>,这几个标签是不支持伪元素的。

所以这里我们输出 DOM 的时候,给 img 的父元素带上部分图片描述标签。通过 CSS 去控制这些标签的展示:

<div class="g-wrap" desc1="商品描述AAA" desc2="商品描述BBB">
    <img src="https://xx.baidu.com/timg?xxx" >    
</div>
[desc1]::before,
[desc2]::after {
    position: absolute;
    opacity: 0;
}

[desc1]::before {
    content: attr(desc1);
}

[desc2]::after {
    content: attr(desc2);
}

[desc1]:hover::before,
[desc2]:hover::after{
    opacity: 1;
}

看看效果:

attributeselector4

CodePen Demo -- 通过属性选择器给图片添加标签

属性选择器配合伪元素实现下载提示

我们知道,HTML5 对标签新增了一个 download 属性,此属性指示浏览器下载 URL 而不是导航到它。

那么,我们可以利用属性选择器对所有带此类标签的元素进行提示。像这样:

<a href="https://www.xxx.com/logo.png" download="logo">logo</a>
[download] {
    position: relative;
    color: hotpink;
}

[download]:hover::before {
    content: "点击可下载此资源!";
    position: absolute;
    ...
}

当我们 hover 到这个链接的时候,就会这样,提示用户,这是一个可以下载的按钮:

attributeselector3

CodePen Demo -- 属性选择器配合伪元素做下载提示

属性选择器配合伪元素对链接的协议进行提示(http/https)

现在大部分网站不是切了 https 就是走在切 https 的路上。如果页面上的链接很多或者对跳转页面的协议有要求,使用属性选择器配合伪元素对链接的协议进行提示也不失为一种好方法。

a[href^="http:"]:hover::before {
    content: "这是一个http链接";
    ...
}

a[href^="https:"]:hover::before {
    content: "这是一个https链接";
}

CodePen Demo -- 属性选择器配合伪元素对链接的协议进行提示(http/https)

当然,伪元素的内容不一定是纯文字的,为了给用户更好的体验,图或者图片加文字也是可以的。

譬如我们可以形象化地给 https 链接站点再加一个小绿锁,符合用户的一些常规认知。

image

这里我将小绿锁的图片使用 base64 嵌入到伪元素当中,简单的使用 text-indent 控制图文的排布:

a[href^="https:"]:hover::before {
    content: "";
    padding-left: 16px;
    background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAb0lEQVQoz2NkQAJc5aIc//7962VgYIiDCi1iYmIq/tb5+gdMDROyBqhiGWYmJlVmJiZVBgYGGagYdsBRKvyZu1xUAsbnLheV4CgV/oxbQ4nwf0JiTAwkAkaIU4QaGf4z1uFX+b/pR/e7epJtGJEaAKDXHzEJ3KYmAAAAAElFTkSuQmCC");
    ...
}

attributeselector5

这里只是一个非常小的 Demo,实际情况是大部分用户并不了解这个小绿锁的含义,所以实际使用应该搭配文字辅助提示。

CodePen Demo -- 属性选择器配合伪元素对https协议进行图文提示

属性选择器对文件类型的处理

也可以对一些可下载资源进行视觉上 icon 的提示。

<ul>
    <li><a href="xxx.doc">Word File</a></li>
    <li><a href="xxx.ppt">PPT File</a></li>
    <li><a href="xxx.PDF">PDF File</a></li>
    <li><a href="xxx.MP3">MP3 File</a></li>
    <li><a href="xxx.avi">AVI File</a></li>
</ul>
a[href$=".doc" i]::before {
    content: "doc";
    background: #a9c4f5;
}
a[href$=".ppt" i]::before {
    content: "ppt";
    background: #f8e94f;
}
a[href$=".pdf" i]::before {
    content: "pdf";
    background: #fb807a;
}
a[href$=".mp3" i]::before {
    content: "mp3";
    background: #cb5cf5;
}
a[href$=".avi" i]::before {
    content: "avi";
    background: #5f8ffc;
}

image

CodePen Demo -- 属性选择器选择文件名后缀

属性选择器对 input 类型的处理

属性选择器其实对 input 类型的元素是一个很好的帮手,因为 input 常用,且经常搭配很多不同功能的属性值。

只不过,由于 input 类型无法添加伪元素。所以搭配属性选择器更多的通过属性的各种状态改变自身的样式。

简单举个例子,譬如:

<input type="text">
<input type="text" disabled>
input[type=text][disabled] { 
    border: 1px solid #aaa;
    background: #ccc; 
}

这里,我们选择了 type=text 并且拥有 disabled 属性的 input 元素,将它的背景色和边框色设置为灰色。给与用户更好的视觉提示。

image

值得注意的点

注意选择器优先级 ,.class[class=xxx] 是否等价

考虑这个问题,下面两个选择器是否等值?

<div class="header">
.header {
    color: red;
}

[class~="header"] {
    color: blue;
}

上述两个选择器,作用完全一致。然而,如果是下面这种情况,两者就不一样了:

<div id="header">
#header{
    color: red;
}

[id="header"] {
    color: blue;
}

这里,ID 选择器#header比属性选择器[id="header"]的权重更高,虽然两者能够选择同样的元素,但是两者并不完全等价。

是否需要引号?

考虑下面三种情况,是否一致?

[class="header"]{ ... }

[class='header']{ ... }

[class=header]{ ... }

事实上,从 HTML2 开始,不添加引号的写法就已经得到支持,所以上述三种写法都是正确的。

然而,能够不使用引号也是有限制的,再看看下面这种写法:

a[href=bar] { ... }

a[href^=http://] {... }

第二个选择器是个无效选择器,:// 不引起来的话会识别错误,必须使用引号引起来像这样a[href^="http://"] ,这里具体的原因可以看看这篇文章:Unquoted attribute value validator

所以保险起见,建议都加上引号。

CSS 语义化

编写”具有语义的HTML”原则是现代、专业前端开发的一个基础。当然,我们经常谈论到的都是 HTML 语义化。

那么,CSS 需要语义化吗?CSS 有语义化吗?例如上述的例子,使用特定的类名或者 id 选择器皆可完成。那么使用属性选择器的理由是什么?

我的理解是,属性(attribute)本身已经具有一定的语义,表达了元素的某些特征或者功能,利用属性选取元素再进行对该属性值的特定操作,一定程度上也可以辅助提升代码的语义化。至少的提升了 CSS 代码的可读性。但是 CSS 是否需要语义化这个问题就见仁见智了。

最后

这里有几篇文章还涵盖了很多其他方面使用,可以对比观看:

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

好了,本文到此结束,希望对你有帮助 :)

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

最后,新开通的公众号求关注,形式希望是更短的篇幅,质量更高一些的技巧类文章,包括但不局限于 CSS:

image

大家都知道,当一些重大事件发生的时候,我们的网站,可能需要置灰,像是这样:

image

当然,通常而言,全站置灰是非常简单的事情,大部分前端同学都知道,仅仅需要使用一行 CSS,就能实现全站置灰的方式。

像是这样,我们仅仅需要给 HTML 添加一个统一的滤镜即可:

html {
    filter: grayscale(.95);
    -webkit-filter: grayscale(.95);
}

又或者,使用 SVG 滤镜,也可以快速实现网站的置灰:

<div>
// ...
</div>

<svg xmlns="https://www.w3.org/2000/svg">
  <filter id="grayscale">
    <feColorMatrix type="matrix" values="0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0"/>
    </filter>
</svg>
html {
    filter: url(#grayscale);
}

大部分时候,这样都可以解决大部分问题。不过,也有一些例外。譬如,如果我们仅仅需要置灰网站的首屏,而当用户开始滚动页面的时候,非首屏部分不需要置灰,像是如下动图所示,该怎么办呢?

看看示意:

这种只置灰首屏的诉求该如何实现呢?

使用 backdrop-filter 实现滤镜遮罩

这里,我们可以借助 backdrop-filter 实现一种遮罩滤镜效果。

filter VS backdrop-filter

在 CSS 中,有两个和滤镜相关的属性 -- filterbackdrop-filter

backdrop-filter 是更为新的规范推出的新属性,可以点击查看 Filter Effects Module Level 2。

  • filter:该属性将模糊或颜色偏移等图形效果应用于元素。
  • backdrop-filter: 该属性可以让你为一个元素后面区域添加图形效果(如模糊或颜色偏移)。 它适用于元素背后的所有元素,为了看到效果,必须使元素或其背景至少部分透明。

注意两者之间的差异,filter 是作用于元素本身,而 backdrop-filter 是作用于元素背后的区域所覆盖的所有元素。而它们所支持的滤镜种类是一模一样的。

backdrop-filter 最为常见的使用方式是用其实现毛玻璃效果。

看这样一段代码:

<div class="bg">
    <div>Normal</div>
    <div class="g-filter">filter</div>
    <div class="g-backdrop-filter">backdrop-filter</div>
</div>
.bg {
    background: url(image.png);
    
    & > div {
        width: 300px;
        height: 200px;
        background: rgba(255, 255, 255, .7);
    }
    .g-filter {
        filter: blur(6px);
    }
    .g-backdrop-filter {
        backdrop-filter: blur(6px);
    }
}

CodePen Demo -- filter 与 backdrop-filter 对比

filterbackdrop-filter 使用上最明显的差异在于:

  • filter 作用于当前元素,并且它的后代元素也会继承这个属性
  • backdrop-filter 作用于元素背后的所有元素

仔细区分理解,一个是当前元素和它的后代元素,一个是元素背后的所有元素

理解了这个,就能够明白为什么有了 filter,还会有 backdrop-filter

使用 backdrop-filter 实现首屏置灰遮罩

这样,我们可以快速的借助 backdrop-filter 实现首屏的置灰遮罩效果:

html {
    position: relative;
    width: 100%;
    height: 100%;
    overflow: scroll;
}
html::before {
    content: "";
    position: absolute;
    inset: 0;
    backdrop-filter: grayscale(95%);
    z-index: 10;
}

仅仅只是这样而已,我们就在整个页面上方叠加了一层滤镜蒙版,实现了只对首屏页面的置灰:

借助 pointer-events: none 保证页面交互

当然,这里有个很严重的问题,我们的页面是存在大量交互效果的,如果叠加了一层遮罩效果在其上,那这层遮罩下方的所有交互事件都将失效,譬如 hover、click 等。

那该如何解决呢?这个也好办,我们可以通过给这层遮罩添加上 pointer-events: none,让这层遮罩不阻挡事件的点击交互。

代码如下:

html::before {
    content: "";
    position: absolute;
    inset: 0;
    backdrop-filter: grayscale(95%);
    z-index: 10;
  + pointer-events: none;
}

CodePen Demo -- Gray Website by backdrop-filter

当然,有同学又会开始质疑了,backdrop-filter 虽好,但是你自己瞅瞅它的兼容性,很多旧版 firefox 不支持啊大哥。我们那么多火狐的用户咋办?

截至至 2022/12/01,Firefox 的最新版本为 109,但是在 Firefox 103 之前,都是不支持 backdrop-filter 的。

别急,除了 filterbackdrop-filter,我们还有方式能够实现网站的置灰。

借助混合模式实现网站置灰

除了 filterbackdrop-filter 外,CSS 中另外一个能对颜色进行一些干预及操作的属性就是 mix-blend-modebackground-blend-mode 了,翻译过来就是混合模式。

如果你对混合模式还比较陌生,可以看看我的这几篇文章

这里,backdrop-filter 的替代方案是使用 mix-blend-mode

看看代码:

html {
    position: relative;
    width: 100%;
    height: 100%;
    overflow: scroll;
    background: #fff;
}
html::before {
    content: "";
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 1);
    mix-blend-mode: color;
    pointer-events: none;
    z-index: 10;
}

我们还是叠加了一层额外的元素在整个页面的首屏,并且把它的背景色设置成了黑色 background: rgba(0, 0, 0, 1),正常而言,我们的网站应该是一片黑色的。

但是,神奇的地方在于,通过混合模式的叠加,也能够实现网站元素的置灰。我们来看看效果:

fil2

经过实测:

{
  mix-blend-mode: hue;            // 色相
  mix-blend-mode: saturation;     // 饱和度
  mix-blend-mode: color;          // 颜色
}

上述 3 个混合模式,叠加黑色背景,都是可以实现内容的置灰的。

值得注意的是,上述方法,我们需要给 HTML 设置一个白色的背景色,同时,不要忘记了给遮罩层添加一个 pointer-events: none

CodePen Demo -- Gray Website By MixBlendMode

总结一下

这里,再简单总结一下。

  1. 如果你需要全站置灰,使用 CSS 的 filter: grayscale()
  2. 对于一些低版本的浏览器,使用 SVG 滤镜通过 filter 引入
  3. 对于仅仅需要首屏置灰的,可以使用 backdrop-filter: grayscale() 配合 pointer-events: none
  4. 对于需要更好兼容性的,使用混合模式的 mix-blend-mode: huemix-blend-mode: saturationmix-blend-mode: color 也都是非常好的方式

最后

好了,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

本文接前文:不可思议的混合模式 mix-blend-mode 。由于 mix-blend-mode 这个属性的强大,很多应用场景和动效的制作不断完善和被发掘出来,遂另起一文继续介绍一些使用 mix-blend-mode 制作的酷炫动画。

CSS3 新增了一个很有意思的属性 -- mix-blend-mode ,其中 mix 和 blend 的中文意译均为混合,那么这个属性的作用直译过来就是混合混合模式,当然,我们我们通常称之为混合模式

混合模式最常见于 photoshop 中,是 PS 中十分强大的功能之一。当然,瞎用乱用混合模式谁都会,利用混合模式将多个图层混合得到一个新的效果,只是要用到恰到好处,或者说在 CSS 中利用混合模式制作出一些效果则需要对混合模式很深的理解及不断的尝试。

mix-blend-mode 简介

关于 mix-blend-mode 最基本的用法和描述,可以简单看看上篇文章 不可思议的混合模式 mix-blend-mode

background-blend-mode 简介

除了 mix-blend-mode ,CSS 还提供一个 background-blend-mode 。也就是背景的混合模式。

  • 可以是背景图片与背景图片的混合,
  • 也可以是背景图片和背景色的之间的混合。

background-blend-mode 的可用取值与 mix-blend-mode一样,不重复介绍,下面直接进入应用阶段。

background-blend-mode 基础应用

对于 background-blend-mode ,最简单的应用就是将两个或者多个图片利用混合模式叠加在一起。假设我们存在下述两张图片,可以利用背景混合模式 background-blend-mode 叠加在一起:

person
timg

经过背景混合模式 background-blend-mode:lighten 处理之后:

image

CodePen Demo -- image mix by bg-blend-mode

当然,这里使用的是 background-blend-mode:lighten 变亮这个混合模式,核心代码如下:

<div class="container"></div>
.container {
    background: url($pic1), url($pic2);
    background-size: cover;
    background-blend-mode: lighten;
}

我们可以尝试其他的组合,也就是改变 background-blend-mode 的各种取值,将会得到各种不同的感官效果。

使用 background-blend-mode: difference 制作黑白反向动画

黑色白色这两种颜色,无疑是使用频率最高也是我认为最搭的两个颜色。当这两种颜色结合在一起,总是能碰撞出不一样的火花。

扯远了,借助 difference 差值混合模式,配合黑白 GIF,能产生奇妙的效果,假设我们拥有这样一张 GIF 图(图片来自网络,侵删):

timg

利用 background-blend-mode: difference ,将它叠加到不同的黑白背景之下(黑白背景由 CSS 画出来):

image

产生的效果如下:

bg-gif

CodePen Demo -- https://codepen.io/Chokcoco/pen/vpLWBW

我们可以尝试其他的组合,将会得到各种不同的感官效果。

使用 background-blend-mode 制作 hover 效果

想象一下,在上面第一个例子中,如果背景的黑白蒙层不是一开始就叠加在 GIF 图下,而是通过某些交互手段叠加上去。

应用这种方式,我们可以使用 background-blend-mode 来制作点击或者 hover 时候的蒙板效果。

假设我们有这样一张原图(黑白效果较好):

image

通过混合渐变背景色,配合 Hover 效果,我们可以给这些图配上一些我们想要的色彩:

bgblendmodehover

CodePen Demo --background-blend-mode && Hover

代码非常简单,示意如下:

.pic {
    width: 300px;
    height: 200px;
    background: url($img),
        linear-gradient(#f00, #00f);
    background-size: cover, 100% 100%;
    background-position: 0 0, -300px 0;
    background-blend-mode: luminosity;
    background-repeat: no-repeat;
    transition: .5s background-position linear;
}

.pic:hover { 
    background-position: 0 0, 0 0; 
}

这里有几点需要注意的:

  • 这里使用了背景色渐变动画背景色的渐变动画有几种方式实现(戳这里了解更多方法),这里使用的是位移 background-position
  • 实现上述效果使用的 background-blend-mode 不限制具体某一种混合模式,可以自己多尝试

使用 mix-blend-mode || background-blend-mode 改变图标的颜色

如果再运用上上一篇文章介绍的知识 两行 CSS 代码实现图片任意颜色赋色技术 ,我们可以实现 ICON 的颜色的动态改变。

假设我们有这样一张 ICON 图,注意主色是黑色,底色的白色(底色不是透明色),所以符合要求的 JPG、PNG、GIF 图都可以:

iconmonstr-cursor-31

利用 background-blend-mode: lighten 可以实现动态改变图标主色的效果:

bgblendhover

而且这里的具体颜色(渐变、纯色皆可),动画方向都可以可以随意控制的。

CodePen Demo -- bg-blend-mode && hover

又或者是这种 hover fadeIn 效果:

bgblendhover2

CodePen Demo -- mix-blend-mode && hover

使用 mix-blend-mode 制作文字背景图

我们将上面 ICON 这个场景延伸一下,ICON 图可以延伸为任意黑色主色白色底色图片,而颜色则可以是纯色、渐变色、或者是图片。

那么我们可以尝试让文字带上渐变色,或者说让文字透出图片。当然这个效果有一些 CSS 属性也可以完成。

譬如 background-clip: text 背景裁剪就可以让文字带上渐变色或者展示图片,可以戳这里看看 使用 background-clip 实现文字渐变

这里我们使用 mix-blend-mode 也能够轻易实现,我们只需要构造出黑色文字,白色底色的文字 div ,叠加上图片,再运用 mix-blend-mode 即可,简单原理如下:

image

核心代码如下,可以看看:

<div class="container">
    <div class="pic"></div>
    <div class="text">IMAGE</div>
</div>
.pic {
    position: relative;
    width: 100%;
    height: 100%;
    background: url($img);
    background-repeat: no-repeat;
    background-size: cover;
}

.text {
    position: absolute;
    width:100%;
    height:100%;
    color: #000;
    mix-blend-mode: lighten;
    background-color: #fff;
}

CodePen Demo -- mix-blend-mode && TEXT IMAGE

最后

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

好了,本文到此结束,希望对你有帮助 :)

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

CSS3 新增了一个很有意思的属性 -- mix-blend-mode ,其中 mix 和 blend 的中文意译均为混合,那么这个属性的作用直译过来就是混合混合模式,当然,我们我们通常称之为混合模式

混合模式最常见于 photoshop 中,是 PS 中十分强大的功能之一。当然,瞎用乱用混合模式谁都会,利用混合模式将多个图层混合得到一个新的效果,只是要用到恰到好处,或者说在 CSS 中利用混合模式制作出一些效果则需要对混合模式很深的理解及不断的尝试。

我个人对混合模式的理解也十分浅显,本文只是带领大家走进 CSS 混合模式的世界,初浅的了解混合模式及尝试使用它制作一些效果。

mix-blend-mode 概述

上文也说了,mix-blend-mode 描述了元素的内容应该与元素的直系父元素的内容和元素的背景如何混合。我们将 PS 中图层的概念替换为 HTML 中的元素。

看看可取的值有哪些:

{
  mix-blend-mode: normal;         // 正常
  mix-blend-mode: multiply;       // 正片叠底
  mix-blend-mode: screen;         // 滤色
  mix-blend-mode: overlay;        // 叠加
  mix-blend-mode: darken;         // 变暗
  mix-blend-mode: lighten;        // 变亮
  mix-blend-mode: color-dodge;    // 颜色减淡
  mix-blend-mode: color-burn;     // 颜色加深
  mix-blend-mode: hard-light;     // 强光
  mix-blend-mode: soft-light;     // 柔光
  mix-blend-mode: difference;     // 差值
  mix-blend-mode: exclusion;      // 排除
  mix-blend-mode: hue;            // 色相
  mix-blend-mode: saturation;     // 饱和度
  mix-blend-mode: color;          // 颜色
  mix-blend-mode: luminosity;     // 亮度
  
  mix-blend-mode: initial;
  mix-blend-mode: inherit;
  mix-blend-mode: unset;
}

除去 initial 默认、inherit 继承 和 unset 还原这 3 个所有 CSS 属性都可以取的值外,还有另外的 16 个具体的取值,对应不同的混合效果。

如果不是专业的 PSer 天天和混合模式打交道,想要记住这么多效果,还是挺困难的。不过有前人帮我们总结了一番,看看如何比较好的理解或者说记忆这些效果,摘自Photoshop中高级进阶系列之一——图层混合模式原理

image

当然,上图是 PS 中的混合模式,数量比 CSS 中的多出几个,但是分类还是通用的。

mix-blend-mode 实例

眼见为实,要会使用 mix-blend-mode ,关键还是要迈出使用这一步。这里我写了一个简单的 Demo,包括了所有的混合模式,可以大概试一下各个模式的效果:

CodePen Demo

当然,仅仅是这样是感受不到混合模式的魅力的,下面就列举几个利用了混合模式制作的 CSS 动画。

使用 mix-blend-mode: screen 滤色模式制作 loading 效果

为了照顾某些访问 codepen 慢同学,特意制作了该效果的 Gif,看看效果:

mixmode-loading

CodePen Demo

这里使用了 mix-blend-mode: screen 滤色模式,这是一种提亮图像形混合模式。滤色的英文是 screen,也就是两个颜色同时投影到一个屏幕上的合成颜色。具体做法是把两个颜色都反相,相乘,然后再反相。简单记忆为"让白更白,而黑不变"。(不一定十分准确,如有错误还请指正)

我们将三个 div 按照不同延时(animation-delay)小幅度旋转起来,来达到一种很显眼很魔性的效果,适合做 loading 图。

使用 mix-blend-mode: difference 差值模式

再举个例子, mix-blend-mode: difference 差值模式。查看每个通道中的颜色信息,比较底色和绘图色,用较亮的像素点的像素值减去较暗的像素点的像素值。与白色混合将使底色反相;与黑色混合则不产生变化。

通俗一点就是上方图层的亮区将下方图层的颜色进行反相,暗区则将颜色正常显示出来,效果与原图像是完全相反的颜色。

看看利用了这个混合模式,运用在一些多图层效果里,可以产生十分绚烂的混合效果:

mixmode-different

CodePen Demo

上图看似复杂,其实了解原理之后非常的简单,6 个旋转的 div ,通过 mix-blend-mode: difference 混合在一起。

使用多混合模式制作文字故障效果

最后,想到我之前制作的一个文字故障效果,也可以很好的融合混合模式,制作出下列效果:

mixmode-word-break

CodePen Demo

不用怀疑你的眼睛,上图的效果是纯 CSS 实现的效果,运用了多种颜色混合模式实现颜色叠加,变亮等效果。

本文涉及的专业理论知识很少,没有用很大的篇幅去描述每一个混合模式的效果及作用。我对混合模式的理解也比较粗浅,本文旨在通过一些 Demo 让读者学会开始去使用这些混合模式效果,俗话说修行在个人,如果真的感兴趣的可以自行深入研究。

最后,看一眼兼容性吧,这种奇妙的属性兼容性通常都不怎么好,我之前几篇文章也提到过了,面向未来编程,所以本文的 CodePen Demo 都要求在 -webkit- 内核浏览器下观看:

image

最后

本文有下半篇:不可思议的混合模式 background-blend-mode,可以配合阅读,效果更好。

在 CSS 中,渐变(Gradient)可谓是最为强大的一个属性之一。

但是,经常有同学在使用渐变的过程中会遇到渐变图形产生的锯齿问题。

何为渐变锯齿?

那么,什么是渐变图形产生的锯齿呢?

简单的一个 DEMO:

<div></div>
div {
    width: 500px;
    height: 100px;
    background: linear-gradient(37deg), #000 50%, #f00 50%, #f00 0);
}

效果如下:

其实,锯齿感已经非常明显了,我们再放大了看,其内部其实是这样的:

又或者是这样:

有意思的是,锯齿现象在 DPR 为 1 的屏幕下特别明显,而在一些高清屏(dpr > 1)的屏幕下,感受不会那么明显。

DPR(Device Pixel Ratio)为设备像素比,DPR = 物理像素 / 设备独立像素,设备像素比描述的是未缩放状态下,物理像素和设备独立像素的初始比例关系。

那么为啥会产生锯齿感呢?

传统网页的呈现是基于像素单位的,对于这种一种颜色直接过渡另外一种颜色状态的图片,容易导致可视质量下降(信息失真)。因而对于普通的渐变元素,像是上述写法,产生了锯齿,这是非常常见的在使用渐变过程中的一个棘手问题。

简单的解决办法

解决失真的问题有很多。这里最简单的方式就是不要直接过渡,保留一个极小的渐变过渡空间。

上述的代码,我们可以简单改造一下:

div {
    width: 500px;
    height: 100px;
  - background: linear-gradient(37deg), #000 50%, #f00 50%, #f00);
  + background: linear-gradient(37deg), #000 49.5%, #f00 50.5%, #f00);
}

仔细看其中的变化,我们从 50% --> 50% 的直接过渡,变化成预留了 1% 的渐变过渡空间,效果如下:

可以看到,效果立马有了大幅提升!

当然,如果不想修改原代码,也可以通过叠加一层伪元素实现,这里给出 3 种方式的对比图:

<div></div>
<div class="gradient"></div>
<div class="pesudo"></div>
:root {
    --deg: 37deg;
    --c1: #000;
    --c2: #f00;
    --line-width: 0.5px;
}
div {
    margin: auto;
    width: 500px;
    height: 100px;
    background: linear-gradient(
        var(--deg),
        var(--c1) 50%,
        var(--c2) 50%,
        var(--c2) 0
    );
}
// 方法一:
.gradient {
    background: linear-gradient(
        var(--deg),
        var(--c1),
        var(--c1) calc(50% - var(--line-width)),
        var(--c2) calc(50% + var(--line-width)),
        var(--c2) 0
    );
}
// 方法二:
.pesudo {
    position: relative;

    &::before {
        content: "";
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: linear-gradient(
            var(--deg),
            transparent,
            transparent calc(50% - var(--line-width)),
            var(--c1) calc(50% - var(--line-width)),
            var(--c2) calc(50% + var(--line-width)),
            transparent calc(50% + var(--line-width)),
            transparent
        );
    }
}

通过伪元素叠加的意思是,在产生锯齿的地方,实现一个平滑过渡进行覆盖:

效果如下:

CodePen Demo -- 消除 Gradient 锯齿

划重点!此方法适用于线性渐变、径向渐变、角向渐变,是最为简单的消除 CSS 锯齿的方式。

更为高阶的锯齿消除法

当然,也还有其他更为高阶的锯齿消除法。

仿生狮子的这篇文章中 -- CSS 幻术 | 抗锯齿,还介绍了另外一种有意思的消除锯齿的方式。以下内容,部分摘录至该文章。

我们可以建立一种边缘锯齿边缘->重建锯齿边缘的锯齿消除方法。

我们需要做的,就是在锯齿处,叠加上另外一层内容,让锯齿感不那么强烈。称为像素偏移抗锯齿(Pixel-Offset Anti-Aliasing,POAA)。

Implementing FXAA这篇博客中,解释了 FXAA 具体是如何运作的。对于一个已经被找到的图形边缘,经过 FXAA 处理后会变成这样,见下两幅图:

FXAA(Fast Approximate Anti-Aliasing),快速近似抗锯齿,它找到画面中所有图形的边缘并进行平滑处理。

我们可以轻易找到找到渐变的边缘地方,就是那些渐变的颜色改变的地方。有了边缘信息后,接着就要重建边缘。重建边缘也许可以再拆分,分为以下几个步骤:

  • 需要通过某种方法得到透明度的点
  • 这些点需要能够组成线段
  • 线段完全吻合我们的 Gradient
  • 使线段覆盖在 Gradient 的上一层以应用我们的修改

这就是大体思路,我们并没有参与浏览器的渲染,而是通过像 FXAA 一样的后处理的方法。在已渲染的图像上做文章。

比如说,我们有这样一张图:

.circle-con {
    $c1: #cd3f4f;
    $c2: #e6a964;
    position: relative;
    height: 300px;
    background-image: repeating-radial-gradient(
        circle at 0% 50%, 
        $c1 0, 
        $c2 50px
    );
}

边缘信息如下:

我们要做的,就是在它的边缘处,利用渐变再生成一段渐变,通过准确叠加,消除渐变!原理图如下:

原理可行,但是实操起来非常之复杂,计算量会比较大。感兴趣的可以拿这段代码尝试一下:

.repeat-con {
    --c1: #cd3f4f;
    --c2: #e6a964;
    --c3: #5996cc;
    position: relative;
    height: 300px;
    background-image: repeating-linear-gradient(
        var(--deg),
        var(--c1),
        var(--c1) 10px,
        var(--c2) 10px,
        var(--c2) 40px,
        var(--c1) 40px,
        var(--c1) 50px,
        var(--c3) 50px,
        var(--c3) 80px
    );

    &.antialiasing {
        &:after {
            --offsetX: 0.4px;
            --offsetY: -0.1px;
            --dark-alpha: 0.3;
            --light-alpha: 0.6;
            --line-width: 0.6px;
            content: '';
            position: absolute;
            top: var(--offsetY);
            left: var(--offsetX);
            width: 100%;
            height: 100%;
            opacity: 0.5;
            background-image: repeating-linear-gradient(
                var(--deg),
                var(--c3),
                transparent calc(0px + var(--line-width)),
                transparent calc(10px - var(--line-width)),
                var(--c2) 10px,
                var(--c1) 10px,
                transparent calc(10px + var(--line-width)),
                transparent calc(40px - var(--line-width)),
                var(--c1) 40px,
                var(--c2) 40px,
                transparent calc(40px + var(--line-width)),
                transparent calc(50px - var(--line-width)),
                var(--c3) 50px,
                var(--c1) 50px,
                transparent calc(50px + var(--line-width)),
                transparent calc(80px - var(--line-width)),
                var(--c1) 80px
            );
        }
    }
}

最后

简单总结一下,本文介绍了几种 CSS 中可行的消除渐变锯齿的方法。

好了,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

鼠标拖拽元素移动,算是一个稍微有点点复杂的交互。本文,就将打破常规,向大家介绍一种超强的仅仅使用纯 CSS 就能够实现的鼠标点击拖拽效果。

在之前的这篇文章中 -- 不可思议的纯 CSS 实现鼠标跟随,我们介绍了非常多有意思的纯 CSS 的鼠标跟随效果,像是这样:

但是,可以看到,上面的效果中,元素的移动不是很丝滑。如果你了解上述的实现方式,就会知道它存在比较大的局限性。

本文,我们还是仅仅通过 CSS,来实现一种丝滑的鼠标点击拖动元素移动的效果。

鼠标点击拖拽跟随效果

OK,什么意思呢?我们先来看一个最最简单的效果示意图,点击一个元素,能够拖动元素进行移动:

好,到这里,在继续往下阅读之前,你可以停一停。这种效果,正常而言,都是必须要借助 JavaScript 才能够实现的。从表现上来看:

  1. 首先拖拽元素,可以任意将元素进行移动
  2. 然后放置元素,让元素停留在另外一个地方

思考一下,如果不借助 JavaScript 的话,有办法将元素小球从 A 点移动到 B 点么?这个效果完全就不像是纯 CSS 能够完成的。

答案必然是可以的!过程也非常之巧妙,这里我们核心需要利用强大的 resize 属性。以及,配合通过构建一种巧妙的布局,去解决可能会遇到的各种难题。

一个简单的 resize 元素

首先,我们利用 resize 属性来实现一个可改变大小的元素。什么是 resize

MDN -- resize:该 CSS 属性允许你控制一个元素的可调整大小性。

语法如下:

{
/* Keyword values */
  resize: none;
  resize: both;
  resize: horizontal;
  resize: vertical;
  resize: block;
  resize: inline;
}

简单解释一下:

  • resize: none:元素不能被用户缩放
  • resize: both:允许用户在水平和垂直方向上调整元素的大小
  • resize: horizontal:允许用户在水平方向上调整元素的大小
  • resize: vertical:允许用户在垂直方向上调整元素的大小
  • resize: block:根据书写模式(writing-mode)和方向值(direction),元素显示允许用户在块方向上(block)水平或垂直调整元素大小的机制。
  • resize: inline:根据书写模式(writing-mode)和方向值(direction),元素显示一种机制,允许用户在内联方向上(inline)水平方向或垂直方向调整元素的大小。

看一个最简单的 DEMO:

<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. A aut qui labore rerum placeat similique hic consequatur tempore doloribus aliquid alias, nobis voluptates. Perferendis, voluptate placeat esse soluta deleniti id!</p>
p {
    width: 200px;
    height: 200px;
    resize: horizontal;
    overflow: scroll;
}

这里,我们设置了一个长宽为 200px<p> 为横向可拖拽改变宽度。效果如下:

简单总结一些小技巧:

  • resize 的生效,需要配合 overflow: scroll,当然,准确的说法是,overflow 不是 visible,或者可以直接作用于替换元素譬如图像、<video><iframe><textarea>
  • 我们可以通过 resizehorizontalverticalboth 来设置横向拖动、纵向拖动、横向纵向皆可拖动。
  • 可以配合容器的 max-widthmin-widthmax-heightmin-height 限制可拖拽改变的一个范围

这里,如果你的对 resize 还有所疑惑,或者想了解更多 resize 的有趣用法,可以看看我的这篇文章:CSS 奇思妙想 | 使用 resize 实现强大的图片拖拽切换预览功能

将 resize 应用到本文实例中

OK,接下来,我们将 resize 实际运用到我们本文的例子中去,首先,我们先简单实现一个 DIV:

<div class="g-resize"></div>
.g-resize {
    width: 100px;
    height: 100px;
    border: 1px solid deeppink;
}

如下,非常普通,没有什么特别的:

但是,通过给这个元素加上 resize: both 以及 overflow: scroll,此时,这个元素的大小就通过元素右下角的 ICON 进行拖动改变。

简单修改下我们的 CSS 代码:

.g-resize {
    width: 100px;
    height: 100px;
    border: 1px solid deeppink;
    resize: both;
    overflow: scroll;
}

这样,我们就得到了一个灵活可以拖动的元素:

是的,我们的整个效果,就需要借助这个特性进行实现。

在此基础上,我们可以尝试将一个元素定位到上面这个可拖动放大缩小的元素的右下角,看着能不能实现上述的效果。

简单加一点代码:

<div class="g-resize"></div>
.g-resize {
    position: relative;
    width: 20px;
    height: 20px;
    resize: both;
    overflow: scroll;
}
.g-resize::before {
    content: "";
    position: absolute;
    bottom: 0;
    right: 0;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: deeppink;
}

我们利用元素的伪元素实现了一个小球,放置在容器的右下角看看效果:

如果我们再把整个设置了 resize: both 的边框隐藏呢?那么效果就会是这样:

Wow,整个效果已经非常的接近了!只是,认真看的话,能够看到一些瑕疵,就是还是能够看到设置了 resize 的元素的这个 ICON:

这个也好解决,在 Chrome 中,我们可以通过另外一个伪元素 ::-webkit-resizer ,设置这个 ICON 的隐藏。

根据 MDN - ::-webkit-resizer,它属于整体的滚动条伪类样式家族中的一员。

其中 ::-webkit-resizer 可以控制出现在某些元素底角的可拖动调整大小的滑块的样式。

所以,这里我就利用这个伪类:

.g-resize {
    position: relative;
    width: 20px;
    height: 20px;
    resize: both;
    overflow: scroll;
}
.g-resize::before {
    content: "";
    position: absolute;
    bottom: 0;
    right: 0;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: deeppink;
}
.g-resize::-webkit-resizer {
    background-color: transparent;
}

这样,这里的核心在于利用了 .g-resize::-webkit-resizer 中的 background-color: transparent,将滑块的颜色设置为了透明色。我们就得到了与文章一开始,一模一样的效果:

解决溢出被裁剪问题

当然,这里有个很致命的问题,如果需要移动的内容,远比设置了 resize 的容器要大,或者其初始位置不在该容器内,超出了的部分因为设置了 overflow: scroll,将无法看到。

因此上述方案存在比较大的缺陷。

举个例子,假设我们需要被拖动的元素不再是一个有这样一个简单的结构:

<div class="g-content"></div>
.g-content {
    width: 100px;
    height: 100px;
    background: black;
    pointer-event: none;
    
    &::before {
        content: "";
        position: absolute;
        width: 20px;
        height: 20px;
        background: yellow;
        border-radius: 50%;    
}

而像是这样,是一个更为复杂的布局内容展示(当然下面展示的也比较简单,实际中可以想象成任意复杂结构内容):

如果将这个结构,扔到上面的 g-resize 中:

<div class="g-resize">
    <div class="g-content"></div>
</div>

那么就会因为设置了 overflow: scroll 的原因,将完全看不到,只剩下一小块:

为了解决这个问题,我们得修改原本的 DOM 结构,另辟蹊径。

方法有很多,譬如可以利用 Grid 布局的一些特性。当然,这里我们只需要巧妙的加多一层,就可以完全解决这个问题。

我们来实现这样一个布局:

<div class="g-container">
    <div class="g-resize"></div>
    <div class="g-content"></div>
</div>

解释一下上述代码,其中:

  1. g-container 设置为绝对定位加上 display: inline-block,这样其盒子大小就可以由内部正常流式布局盒子的大小撑开
  2. g-resize 设置为 position: relative 并且设置 resize,负责提供一个可拖动大小元素,在这个元素的变化过程中,就能动态改变父容器的高宽
  3. g-content 实际内容盒子,通过 position: absolute 定位到容器的右下角即可

看看完整的 CSS 代码:

.g-container {
    position: absolute;
    display: inline-block;
}
.g-resize { 
    content: "";
    position: relative;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    resize: both;
    overflow: scroll;
    z-index: 1;
}
.g-content {
    position: absolute;
    bottom: -80px;
    right: -80px;
    width: 100px;
    height: 100px;
    background: black;
    pointer-event: none;
    
    &::before {
        content: "";
        position: absolute;
        width: 20px;
        height: 20px;
        background: yellow;
        border-radius: 50%;
        transition: .3s;
    }
}
.g-container:hover .g-content::before {
    transform: scale(1.1);
    box-shadow: -2px 2px 4px -4px #333, -4px 4px 8px -4px #333;
}
.g-resize::-webkit-resizer {
    background-color: transparent;
}

下图中,你看到的所有元素,都只是 g-content 呈现出来的元素,整个效果就是这样:

是的,可能你会有所疑惑,下面我用简单不同颜色,标识不同不同的 DOM 结构,方便你去理解。

  1. 红色边框表示整个 g-container 的大小
  2. 用蓝色矩形表示设置了 g-resize 元素的大小
  3. 关掉 ::-webkit-resizer 的透明设置,展示出 resize 框的可拖拽 ICON
.g-container {
    border: 3px solid red;
}
.g-resize { 
    content: "";
    background: blue;
    resize: both;
    overflow: scroll;
}
.g-resize::-webkit-resizer {
    // background-color: transparent;
}

看看这个图,整个原理基本就比较清晰的浮现了出来:

完整的原理代码,你可以戳这里:CodePen Demo -- Pure CSS Auto Drag Demo

实际应用

OK,用了比较大篇幅对原理进行了描述。下面我们举一个实际的应用场景。使用上述技巧制作的可拖动便签贴。灵感来自 -- scottkellum

代码也不多,如果你了解了上面的内容,下面的代码将非常好理解:

<div class="g-container">
    <div class="g-resize"></div>
    <div class="g-content"> Lorem ipsum dolor sit amet consectetur?</div>
</div>

完整的 CSS 代码如下:

body {
    position: relative;
    padding: 10px;
    background: url("背景图");
    background-size: cover;
}
.g-container {
    position: absolute;
    display: inline-block;
}
.g-resize {
    content: "";
    position: relative;
    width: 20px;
    height: 20px;
    resize: both;
    overflow: scroll;
    z-index: 1;
}
.g-content {
    position: absolute;
    bottom: -160px;
    right: -180px;
    color: rgba(#000, 0.8);
    background-image: linear-gradient(
        160deg,
        rgb(255, 222, 30) 50%,
        rgb(255, 250, 80)
    );
    width: 200px;
    height: 180px;
    pointer-event: none;
    text-align: center;
    font-family: "marker felt", "comic sans ms", sans-serif;
    font-size: 24px;
    line-height: 1.3;
    padding: 1em;
    box-sizing: border-box;
    &:before {
        content: "";
        position: absolute;
        width: 20px;
        height: 20px;
        top: 0;
        left: 0;
        border-radius: 50%;
        background-image: radial-gradient(
            at 60% 30%,
            #f99,
            red 20%,
            rgb(180, 8, 0)
        );
        background-position: 20% 10%;
        cursor: pointer;
        pointer-events: none;
        transform: scale(0.8);
        box-shadow: -5px 10px 3px -8.5px #000, -1px 7px 12px -5px #000;
        transition: all 0.3s ease;
        transform: scale(0.8);
    }
}
.g-container:hover .g-content::before {
    transform: scale(0.9);
    box-shadow: -5px 10px 6px -8.5px #000, -1px 7px 16px -4px #000;
}
.g-resize::-webkit-resizer {
    background-color: transparent;
}

我们通过上述的技巧,实现了一个仅仅使用 CSS 实现的自由拖拽的便签贴。我们可以自由的将其拖拽到任意地方。看看效果:

当然,我们可以再配合上另外一个有意思是 HTML 属性 -- contenteditable

contenteditable 是一个 HTML TAG 的属性,表示元素是否可被用户编辑。如果可以,浏览器会修改元素的部件以允许编辑。

简单修改一下 DOM 结构:

<div class="g-container">
    <div class="g-resize"></div>
    <div class="g-content" contenteditable="true"> Lorem ipsum dolor sit amet consectetur?</div>
</div>

此时,元素不仅可以被拖动,甚至可以被重写,感受一下:

纯 CSS 实现的效果,非常的有意思,完整的代码,你可以戳这里:Pure CSS Auto Drag Demo

最后

其实还有很多有意思的用法,感兴趣的同学可以自己动手,更多的去尝试,组合。

好了,本文到此结束,希望本文对你有所帮助 :)

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

对 Chrome 扩展功能熟悉的小伙伴,可能都有用过 Chrome 的 3D 展示页面层级关系这个功能。

可以通过 控制台 --> 右边的三个小点 --> More Tools --> Layers 打开。即可以看到页面的一个 3D 层级关系,像是这样:

这个功能有几个不错的作用:

  1. 页面层级概览
  2. 快速厘清页面 z-index 层级之间的关系
  3. 用于排查一些重绘过程(滚动过程)页面卡顿

当然,也会存在一些问题,譬如当页面的 DOM 数量太多的时候,这个插件有的时候就会卡到无法交互了。同时,虽然可以快速厘清页面 z-index 层级之间的关系,但是有的时候没法很好的快速看清整个页面嵌套关系。

同时,它只能看整个页面的概览,无法选取部分节点进行观察。

本文,就将介绍一种,快速通过 CSS,构建页面深度关系的 3D 视图,快速清晰的厘清页面 DOM 层级及深度之间的关系。并且可以运用在不同的节点单独进行观察。

当然,总体而言,是基于:

  1. CSS 选择器
  2. CSS 3D 属性

的一次大规模综合应用,整体看完,相信你能学到不少东西。

使用 CSS 构建 3D 可视化 DOM 结构视图

假设,我们首先随时实现一段 DOM 结构,其简单的代码如下:

<div class="g-wrap">
    <div class="g-header">This is Header</div>
    <div class="g-content">
        <div class="g-inner">
            <div class="g-box">Lorem LOrem</div>
            <div class="g-box">Lorem LOrem</div>
        </div>
    </div>
    <div class="g-footer">This is Footer</div>
</div>

部分 CSS 代码:

.g-wrap {
    margin: auto;
    width: 300px;
    height: 500px;
    background: #ddd;
    display: flex;
    align-content: flex-start;
    flex-wrap: wrap;
    flex-direction: column;
    gap: 10px;
    padding: 10px;

    & > div {
        width: 100%;
        flex-grow: 1;
        border: 1px solid #333;
    }
}

.g-content {
    height: 200px;
    display: flex;
    padding: 10px;
    box-sizing: border-box;
    
    .g-inner {
        display: flex;
        padding: 10px;
        gap: 10px;
        
        & > div {
            width: 100px;
            height: 50px;
            border: 1px solid #333;
        }
    }
}

得到这样一个最多深度为 4 层的简单结构:

而我们希望,快速看这个页面的 3D 深度图,像是这样:

又或者,可以使用类似于这样一种 Hover 的交互效果,实现 Hover 某一个 Div,展示出它当前的一个 3D 深度结构图,看看效果:

很有意思的一个效果,到这里应该能明白我们想做一个什么东西了。总的来说,我们的核心需求就是,无论页面的 DOM 结构如何,深度如何,我们希望能够通过一种简单的处理(纯 CSS 实现),能够快速查看页面的 3D 深度结构视图

利用强大的 CSS 选择器,批量处理样式

整个效果看似复杂,其实可以利用 CSS 选择器,很方便的递归调用自己。

因为希望我们的效果可以任意从某一个 DOM 节点处开始,所以,首先,我们需要一个根 CSS 节点,简单的取个名字,为 .g-3d-visual

那么整个 3D 化的样式,我们都会写在 .g-3d-visual 的作用域下:

.g-3d-visual {
    // ...
}

为了让整个代码更易理解,我们会用上 SASS 这种预处理器,主要是利用它的选择器可以的嵌套特性。

至此,我们可以开始构建我们的基础样式,首先我们会处理 2 点:

  1. 整个效果,会稍微的 3D 化,因此会给 .g-3d-visual 根元素添加 3D 相关的样式,譬如 transform-style: preserve-3d,让整个内部元素可以 3D 化
  2. 可以利用通配选择符 *,对 .g-3d-visual 下的所有元素做一个快速的统一处理

那么到这一步,我们的 CSS 代码大概会是这样:

.g-3d-visual {
    transform-style: preserve-3d;
    transform: rotateY(-30deg) rotateX(30deg);

    * {
        position: relative;
        transform-style: preserve-3d;
        transform: translateZ(0);
    }
}

整个图形就变成了这样:

虽然变化不是很多,但是我们已经通过 * 通配符,对内部所有的元素都进行了简单的处理。

图形 3D 化

下一步其实就非常关键了。

我们需要用到元素本身,和元素的两个伪元素,构建元素的立体效果。

举个例子,对于这一块图形:

它的构成是由:

  1. 主体部分由元素本身构成,并且对于结构的每一层,我们通过添加 transform: translateZ(16px),产生不一样的深度
  2. 右侧和下侧的两个面,刚好由元素的两个伪元素通过 transform 旋转不同的角度得到
  3. 整体颜色的调整及阴影

看看代码:

.g-3d-visual {
    transform-style: preserve-3d;
    transform: rotateY(-30deg) rotateX(30deg);

    * {
        position: relative;
        transform-style: preserve-3d;
        background: rgba(0, 0, 255, 0.2);
        transform: translateZ(16px);
        box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.1);

        &::before,
        &::after {
            content: "";
            display: block;
            position: absolute;
            background: rgba(0, 0, 255, 0.2);
        }

        &::before {
            width: 100%;
            height: 16px;
            left: 0;
            bottom: 0;
            transform-origin: center bottom;
            transform: scaleY(1) rotateX(90deg);
        }

        &::after {
            width: 16px;
            height: 100%;
            right: 0;
            top: 0;
            transform-origin: right center;
            transform: scaleX(1) rotateY(-90deg);
        }
    }
}

那么,其实到这里,基本上可以说核心代码都有了,最为核心的是需要理解:

  1. 我们给 .g-3d-visual 下每一层的元素,也就是 * 通配符选择的元素,都添加了一个 transform: translateZ(16px),这一点非常重要,是为了给元素逐渐增加 Z 轴方向的深度
  2. 两个伪元素的运用需要好好理解,它们是用于构建整体的 3D 效果的关键因素
  3. box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.1) 这一个小小的阴影效果的添加,让整个效果看起来更加的真实

这样,我们利用 3 个面,加上简单的阴影,构建了一块一块的立体效果,我们看看目前为止的效果:

按照上述说的,我们可以希望换一种交互方式,实现当鼠标 Hover 到 DOM 的某一层级时,才触发元素 3D 深度变换。

简单改造下代码即可,并且,对于一些重复用到的元素,也可以再利用 CSS 变量统一一下。至此,我们的完整 CSS 代码:

<div class="g-wrap g-3d-visual">
    <div class="g-header">This is Header</div>
    <div class="g-content">
        <div class="g-inner">
            <div class="g-box">Lorem LOrem</div>
            <div class="g-box">Lorem LOrem</div>
        </div>
    </div>
    <div class="g-footer">This is Footer</div>
</div>
:root {
    --side-height: 16px;
    --hover-color: rgba(0, 0, 255, 0.2);
    --box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.1);
    --transform-duration: 0.3s;
}

.g-3d-visual {
    transform-style: preserve-3d;
    transform: rotateY(-30deg) rotateX(30deg);

    * {
        position: relative;
        transform-style: preserve-3d;
        transform: translateZ(0);
        transition: transform var(--transform-duration);
        cursor: pointer;

        &::before,
        &::after {
            content: "";
            display: block;
            position: absolute;
            background: transparent;
            transition: all var(--transform-duration);
        }

        &::before {
            width: 100%;
            height: var(--side-height);
            left: 0;
            bottom: 0;
            transform-origin: center bottom;
            transform: scaleY(0) rotateX(90deg);
        }

        &::after {
            width: var(--side-height);
            height: 100%;
            right: 0;
            top: 0;
            transform-origin: right center;
            transform: scaleX(0) rotateY(-90deg);
        }

        &:hover {
            background: var(--hover-color);
            transform: translateZ(var(--side-height));
            box-shadow: var(--box-shadow);

            &::before,
            &::after {
                background: var(--hover-color);
            }

            &::before {
                transform: scaleY(1) rotateX(90deg);
            }

            &::after {
                transform: scaleX(1) rotateY(-90deg);
            }
        }
    }
}

这样,我们也就得到了题图一开始的 Hover 示意图的效果:

CodePen Demo -- 3D Visualization of DOM

扩展迁移

有了上述代码之后,由于是 SASS 代码,所以记得编译一下,即可拿到完整的 .g-3d-visual 下相关的所有 CSS 代码。

尝试把整段 CSS 代码注入到任意页面后,给你希望观察的节点,添加上 .g-3d-visual 样式即可。

这里我尝试的是,当前正在写作的 Github Issues 页面,看看效果:

当然,可能颜色没有搭配的特别好,但是要知道,整儿页面的 DOM 结构是相当之复杂的。不过整体效果还是很不错的,而且实际操作的过程中,也并不会感觉卡顿。

这一段简单的代码,再简单改造一番,譬如和 Chrome 扩展相结合,快速注入代码,快速指定给哪个元素添加 .g-3d-visual 类名,以及修改配色方案等等,就可以实现一个快速对页面层级进行观察的小插件!

上述效果我是手动修改了当前页面的 HTML 代码,注入的相应的 CSS 代码 :)

总结一下

到这里,我们即可以再简单总结一下完整的步骤:

  • 需要一个整体的 3D 效果,因此需要一个根 CSS 节点,为 .g-3d-visual,并且给它设置好相关的 CSS 3D 属性值,让整个内部元素可以 3D 化
  • 利用通配选择符 *,对 .g-3d-visual 下的所有元素做一个快速的统一处理
  • 利用每个元素的另外两个伪元素,实现每一层效果的 3D 立体感,并且逐层利用 translateZ() 递进深度
  • 通过 :hovertransition 等设置,实现整体的交互效果

当然,这种做法肯定会有一些小问题,譬如如果元素的伪元素已经使用了,那么在 3D 化的效果中,将会被改写。但是由于不是完全覆盖,因此可能会造成一些样式错误。

其次,如果父子两层 DIV 完全是大小一模一样完全重叠在一起,在视觉上也会有些影响。

最后,完整的代码,你可以戳这里获取:CodePen Demo -- 3D Visualization of DOM

最后

好了,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

本文将比较全面细致的梳理一下 CSS 动画的方方面面,针对每个属性用法的讲解及进阶用法的示意,希望能成为一个比较好的从入门到进阶的教程。

CSS 动画介绍及语法

首先,我们来简单介绍一下 CSS 动画。

最新版本的 CSS 动画由规范 -- CSS Animations Level 1 定义。

CSS 动画用于实现元素从一个 CSS 样式配置转换到另一个 CSS 样式配置。

动画包括两个部分: 描述动画的样式规则和用于指定动画开始、结束以及中间点样式的关键帧。

简单来说,看下面的例子:

div {
    animation: change 3s;
}

@keyframes change {
    0% {
        color: #f00;
    }
    100% {
        color: #000;
    }
}
  1. animation: move 1s 部分就是动画的第一部分,用于描述动画的各个规则;
  2. @keyframes move {} 部分就是动画的第二部分,用于指定动画开始、结束以及中间点样式的关键帧;

一个 CSS 动画一定要由上述两部分组成。

CSS 动画的语法

接下来,我们简单看看 CSS 动画的语法。

创建动画序列,需要使用 animation 属性或其子属性,该属性允许配置动画时间、时长以及其他动画细节,但该属性不能配置动画的实际表现,动画的实际表现是由 @Keyframes 规则实现。

animation 的子属性有:

  • animation-name:指定由 @Keyframes 描述的关键帧名称。
  • animation-duration:设置动画一个周期的时长。
  • animation-delay:设置延时,即从元素加载完成之后到动画序列开始执行的这段时间。
  • animation-direction:设置动画在每次运行完后是反向运行还是重新回到开始位置重复运行。
  • animation-iteration-count:设置动画重复次数, 可以指定 infinite 无限次重复动画
  • animation-play-state:允许暂停和恢复动画。
  • animation-timing-function:设置动画速度, 即通过建立加速度曲线,设置动画在关键帧之间是如何变化。
  • animation-fill-mode:指定动画执行前后如何为目标元素应用样式
  • @Keyframes 规则,当然,一个动画想要运行,还应该包括 @Keyframes 规则,在内部设定动画关键帧

其中,对于一个动画:

  • 必须项animation-nameanimation-duration@keyframes规则
  • 非必须项animation-delayanimation-directionanimation-iteration-countanimation-play-stateanimation-timing-functionanimation-fill-mode,当然不是说它们不重要,只是不设置时,它们都有默认值

上面已经给了一个简单的 DEMO, 就用上述的 DEMO,看看结果:

这就是一个最基本的 CSS 动画,本文将从 animation 的各个子属性入手,探究 CSS 动画的方方面面。

animation-name / animation-duration 详解

整体而言,单个的 animation-nameanimation-duration 没有太多的技巧,非常好理解,放在一起。

首先介绍一下 animation-name,通过 animation-name,CSS 引擎将会找到对应的 @Keyframes 规则。

当然,它和 CSS 规则命名一样,也存在一些骚操作。譬如,他是支持 emoji 表情的,所以代码中的 animation-name 命名也可以这样写:

div {
    animation: 😄 3s;
}

@keyframes 😄 {
    0% {
        color: #f00;
    }
    100% {
        color: #000;
    }
}

animation-duration 设置动画一个周期的时长,上述 DEMO 中,就是设定动画整体持续 3s,这个也非常好理解。

animation-delay 详解

animation-delay 就比较有意思了,它可以设置动画延时,即从元素加载完成之后到动画序列开始执行的这段时间。

简单的一个 DEMO:

<div></div>
<div></div>
div {
    width: 100px;
    height: 100px;
    background: #000;
    animation-name: move;
    animation-duration: 2s;
}

div:nth-child(2) {
    animation-delay: 1s;
}
@keyframes move {
    0% {
        transform: translate(0);
    }
    100% {
        transform: translate(200px);
    }
}

比较下列两个动画,一个添加了 animation-delay,一个没有,非常直观:

上述第二个 div,关于 animation 属性,也可以简写为 animation: move 2s 1s,第一个时间值表示持续时间,第二个时间值表示延迟时间。

animation-delay 可以为负值

关于 animation-delay,最有意思的技巧在于,它可以是负数。也就是说,虽然属性名是动画延迟时间,但是运用了负数之后,动画可以提前进行

假设我们要实现这样一个 loading 动画效果:

有几种思路:

  1. 初始 3 个球的位置就是间隔 120°,同时开始旋转,但是这样代码量会稍微多一点
  2. 另外一种思路,同一个动画,3 个元素的其中两个延迟整个动画的 1/3,2/3 时间出发

方案 2 的核心伪代码如下:

.item:nth-child(1) {
    animation: rotate 3s infinite linear;
}
.item:nth-child(2) {
    animation: rotate 3s infinite 1s linear;
}
.item:nth-child(3) {
    animation: rotate 3s infinite 2s linear;
}

但是,在动画的前 2s,另外两个元素是不会动的,只有 2s 过后,整个动画才是我们想要的:

此时,我们可以让第 2、3 个元素的延迟时间,改为负值,这样可以让动画延迟进行 -1s-2s,也就是提前进行 1s2s

.item:nth-child(1) {
    animation: rotate 3s infinite linear;
}
.item:nth-child(2) {
    animation: rotate 3s infinite -1s linear;
}
.item:nth-child(3) {
    animation: rotate 3s infinite -2s linear;
}

这样,每个元素都无需等待,直接就是运动状态中的,并且元素间隔位置是我们想要的结果:

利用 animation-duration 和 animation-delay 构建随机效果

还有一个有意思的小技巧。

同一个动画,我们利用一定范围内随机的 animation-duration 和一定范围内随机的 animation-delay,可以有效的构建更为随机的动画效果,让动画更加的自然。

我在下述两个纯 CSS 动画中,都使用了这样的技巧:

  1. 纯 CSS 实现华为充电动画

纯 CSS 实现华为充电动画

  1. 纯 CSS 实现火焰动画

纯 CSS 实现火焰动画

纯 CSS 实现华为充电动画为例子,简单讲解一下。

仔细观察这一部分,上升的一个一个圆球,抛去这里的一些融合效果,只关注不断上升的圆球,看着像是没有什么规律可言:

我们来模拟一下,如果是使用 10 个 animation-durationanimation-delay 都一致的圆的话,核心伪代码:

<ul>
    <li></li>
    <!--共 10 个...--> 
    <li></li>
</ul>
ul {
    display: flex;
    flex-wrap: nowrap;
    gap: 5px;
}
li {
    background: #000;
    animation: move 3s infinite 1s linear;
}
@keyframes move {
    0% {
        transform: translate(0, 0);
    }
    100% {
        transform: translate(0, -100px);
    }
}

这样,小球的运动会是这样的整齐划一:

要让小球的运动显得非常的随机,只需要让 animation-durationanimation-delay 都在一定范围内浮动即可,改造下 CSS:

@for $i from 1 to 11 {
    li:nth-child(#{$i}) {
        animation-duration: #{random(2000)/1000 + 2}s;
        animation-delay: #{random(1000)/1000 + 1}s;
    }
}

我们利用 SASS 的循环和 random() 函数,让 animation-duration 在 2-4 秒范围内随机,让 animation-delay 在 1-2 秒范围内随机,这样,我们就可以得到非常自然且不同的上升动画效果,基本不会出现重复的画面,很好的模拟了随机效果:

CodePen Demo -- 利用范围随机 animation-duration 和 animation-delay 实现随机动画效果

animation-timing-function 缓动函数

缓动函数在动画中非常重要,它定义了动画在每一动画周期中执行的节奏。

缓动主要分为两类:

  1. cubic-bezier-timing-function 三次贝塞尔曲线缓动函数
  2. step-timing-function 步骤缓动函数(这个翻译是我自己翻的,可能有点奇怪)

三次贝塞尔曲线缓动函数

首先先看看三次贝塞尔曲线缓动函数。在 CSS 中,支持一些缓动函数关键字。

/* Keyword values */
animation-timing-function: ease;  // 动画以低速开始,然后加快,在结束前变慢
animation-timing-function: ease-in;  // 动画以低速开始
animation-timing-function: ease-out; // 动画以低速结束
animation-timing-function: ease-in-out; // 动画以低速开始和结束
animation-timing-function: linear; // 匀速,动画从头到尾的速度是相同的

关于它们之间的效果对比:

除了 CSS 支持的这 5 个关键字,我们还可以使用 cubic-bezier() 方法自定义三次贝塞尔曲线:

animation-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1);

这里有个非常好用的网站 -- cubic-bezier 用于创建和调试生成不同的贝塞尔曲线参数。

三次贝塞尔曲线缓动对动画的影响

关于缓动函数对动画的影响,这里有一个非常好的示例。这里我们使用了纯 CSS 实现了一个钟的效果,对于其中的动画的运动,如果是 animation-timing-function: linear,效果如下:

b

而如果我们我把缓动函数替换一下,变成 animation-timing-function: cubic-bezier(1,-0.21,.85,1.29),它的曲线对应如下:

整个钟的动画律动效果将变成这样,完全不一样的感觉:

CodePen Demo - 缓动不同效果不同

对于许多精益求精的动画,在设计中其实都考虑到了缓动函数。我很久之前看到过一篇《基于物理学的动画用户体验设计》,可惜如今已经无法找到原文。其中传达出的一些概念是,动画的设计依据实际在生活中的表现去考量。

譬如 linear 这个缓动,实际应用于某些动画中会显得很不自然,因为由于空气阻力的存在,程序模拟的匀速直线运动在现实生活中是很难实现的。因此对于这样一个用户平时很少感知到的运动是很难建立信任感的。这样的匀速直线运动也是我们在进行动效设计时需要极力避免的。

步骤缓动函数

接下来再讲讲步骤缓动函数。在 CSS 的 animation-timing-function 中,它有如下几种表现形态:

{
    /* Keyword values */
    animation-timing-function: step-start;
    animation-timing-function: step-end;

    /* Function values */
    animation-timing-function: steps(6, start)
    animation-timing-function: steps(4, end);
}

在 CSS 中,使用步骤缓动函数最多的,就是利用其来实现逐帧动画。假设我们有这样一张图(图片大小为 1536 x 256,图片来源于网络):

可以发现它其实是一个人物行进过程中的 6 种状态,或者可以为 6 帧,我们利用 animation-timing-function: steps(6) 可以将其用一个 CSS 动画串联起来,代码非常的简单:

<div class="box"></div>
.box {
  width: 256px;
  height: 256px;
  background: url('https://github.com/iamalperen/playground/blob/main/SpriteSheetAnimation/sprite.png?raw=true');
  animation: sprite .6s steps(6, end) infinite;
}
@keyframes sprite {
  0% { 
    background-position: 0 0;
  }
  100% { 
    background-position: -1536px 0;
  }
}

简单解释一下上述代码,首先要知道,刚好 256 x 6 = 1536,所以上述图片其实可以刚好均分为 6 段:

  1. 我们设定了一个大小都为 256px 的 div,给这个 div 赋予了一个 animation: sprite .6s steps(6) infinite 动画;
  2. 其中 steps(6) 的意思就是将设定的 @Keyframes 动画分为 6 次(6帧)执行,而整体的动画时间是 0.6s,所以每一帧的停顿时长为 0.1s
  3. 动画效果是由 background-position: 0 0background-position: -1536px 0,由于上述的 CSS 代码没有设置 background-repeat,所以其实 background-position: 0 0 是等价于 background-position: -1536px 0,就是图片在整个动画过程中推进了一轮,只不过每一帧停在了特点的地方,一共 6 帧;

将上述 1、2、3,3 个步骤画在图上简单示意:

从上图可知,其实在动画过程中,background-position 的取值其实只有 background-position: 0 0background-position: -256px 0background-position: -512px 0 依次类推一直到 background-position: -1536px 0,由于背景的 repeat 的特性,其实刚好回到原点,由此又重新开始新一轮同样的动画。

所以,整个动画就会是这样,每一帧停留 0.1s 后切换到下一帧(注意这里是个无限循环动画),:

完整的代码你可以戳这里 -- CodePen Demo -- Sprite Animation with steps()

animation-duration 动画长短对动画的影响

在这里再插入一个小章节,animation-duration 动画长短对动画的影响也是非常明显的。

在上述代码的基础上,我们再修改 animation-duration,缩短每一帧的时间就可以让步行的效果变成跑步的效果,同理,也可以增加每一帧的停留时间。让每一步变得缓慢,就像是在步行一样。

需要提出的是,上文说的每一帧,和浏览器渲染过程中的 FPS 的每一帧不是同一个概念。

看看效果,设置不同的 animation-duration 的效果(这里是 0.6s -> 0.2s),GIF 录屏丢失了一些关键帧,实际效果会更好点:

当然,在 steps() 中,还有 steps(6, start)steps(6, end) 的差异,也就是其中关键字 startend 的差异。对于上述的无限动画而言,其实基本是可以忽略不计的,它主要是控制动画第一帧的开始和持续时长,比较小的一个知识点但是想讲明白需要比较长的篇幅,限于本文的内容,在这里不做展开,读者可以自行了解。

同个动画效果的补间动画和逐帧动画演绎对比

上述的三次贝塞尔曲线缓动和步骤缓动,其实就是对应的补间动画和逐帧动画。

对于同个动画而言,有的时候两种缓动都是适用的。我们在具体使用的时候需要具体分析选取。

假设我们用 CSS 实现了这样一个图形:

现在想利用这个图形制作一个 Loading 效果,如果利用补间动画,也就是三次贝塞尔曲线缓动的话,让它旋转起来,得到的效果非常的一般:

.g-container{
    animation: rotate 2s linear infinite;
}
@keyframes rotate {
    0% {
        transform: rotate(0);
    }
    100% {
        transform: rotate(360deg);
    }
}

动画效果如下:

但是如果这里,我们将补间动画换成逐帧动画,因为有 20 个点,所以设置成 steps(20),再看看效果,会得到完全不一样的感觉:

.g-container{
    animation: rotate 2s steps(20) infinite;
}
@keyframes rotate {
    0% {
        transform: rotate(0);
    }
    100% {
        transform: rotate(360deg);
    }
}

动画效果如下:

整个 loading 的圈圈看上去好像也在旋转,实际上只是 20 帧关键帧在切换,整体的效果感觉更适合 Loading 的效果。

因此,两种动画效果都是很有必要掌握的,在实际使用的时候灵活尝试,选择更适合的。

上述 DEMO 效果完整的代码:CodePen Demo -- Scale Loading steps vs linear

animation-play-state

接下来,我们讲讲 animation-play-state,顾名思义,它可以控制动画的状态 -- 运行或者暂停。类似于视频播放器的开始和暂停。是 CSS 动画中有限的控制动画状态的手段之一。

它的取值只有两个(默认为 running):

{
    animation-play-state: paused | running;
}

使用起来也非常简单,看下面这个例子,我们在 hover 按钮的时候,实现动画的暂停:

<div class="btn stop">stop</div>
<div class="animation"></div>
.animation {
    width: 100px;
    height: 100px;
    background: deeppink;
    animation: move 2s linear infinite alternate;
}

@keyframes move {
    100% {
        transform: translate(100px, 0);
    }
}

.stop:hover ~ .animation {
    animation-play-state: paused;
}

一个简单的 CSS 动画,但是当我们 hover 按钮的时候,给动画元素添加上 animation-play-state: paused

animation-play-state 小技巧,默认暂停,点击运行

正常而言,按照正常思路使用 animation-play-state: paused 是非常简单的。

但是,如果我们想创造一些有意思的 CSS 动画效果,不如反其道而行之。

我们都知道,正常情况下,动画应该是运行状态,那如果我们将一些动画的默认状态设置为暂停,只有当鼠标点击或者 hover 的时候,才设置其 animation-play-state: running,这样就可以得到很多有趣的 CSS 效果。

看个倒酒的例子,这是一个纯 CSS 动画,但是默认状态下,动画处于 animation-play-state: paused,也就是暂停状态,只有当鼠标点击杯子的时,才设置 animation-play-state: running,让酒倒下,利用 animation-play-state 实现了一个非常有意思的交互效果:

完整的 DEMO 你可以戳这里:CodePen Demo -- CSS Beer!

在非常多 Web 创意交互动画我们都可以看到这个技巧的身影。

  1. 页面 render 后,无任何操作,动画不会开始。只有当鼠标对元素进行 click ,通过触发元素的 :active 伪类效果的时候,赋予动画 animation-play-state: running,动画才开始进行;
  2. 动画进行到任意时刻,鼠标停止点击,伪类消失,则动画停止;

animation-fill-mode 控制元素在各个阶段的状态

下一个属性 animation-fill-mode,很多人会误认为它只是用于控制元素在动画结束后是否复位。这个其实是不准确的,不全面的。

看看它的取值:

{
    // 默认值,当动画未执行时,动画将不会将任何样式应用于目标,而是使用赋予给该元素的 CSS 规则来显示该元素的状态
    animation-fill-mode: none;
    // 动画将在应用于目标时立即应用第一个关键帧中定义的值,并在 `animation-delay` 期间保留此值,
    animation-fill-mode: backwards; 
    // 目标将保留由执行期间遇到的最后一个关键帧计算值。 最后一个关键帧取决于 `animation-direction` 和 `animation-iteration-count`
    animation-fill-mode: forwards;    
    // 动画将遵循 `forwards` 和 `backwards` 的规则,从而在两个方向上扩展动画属性
    animation-fill-mode: both; 
}

对于 animation-fill-mode 的解读,我在 Segment Fault 上的一个问答中(SF - 如何理解 animation-fill-mode)看到了 4 副很好的解读图,这里借用一下:

假设 HTML 如下:

<div class="box"></div>

CSS如下:

.box{
    transform: translateY(0);
}
.box.on{
    animation: move 1s;
}

@keyframes move{
    from{transform: translateY(-50px)}
    to  {transform: translateY( 50px)}
}

使用图片来表示 translateY 的值与 时间 的关系:

  • 横轴为表示 时间,为 0 时表示动画开始的时间,也就是向 box 加上 on 类名的时间,横轴一格表示 0.5s
  • 纵轴表示 translateY 的值,为 0 时表示 translateY 的值为 0,纵轴一格表示 50px
  1. animation-fill-mode: none 表现如图:

一句话总结,元素在动画时间之外,样式只受到它的 CSS 规则限制,与 @Keyframes 内的关键帧定义无关。

  1. animation-fill-mode: backwards 表现如图:

一句话总结,元素在动画开始之前(包含未触发动画阶段及 animation-delay 期间)的样式为动画运行时的第一帧,而动画结束后的样式则恢复为 CSS 规则设定的样式。

  1. animation-fill-mode: forwards 表现如图:

一句话总结,元素在动画开始之前的样式为 CSS 规则设定的样式,而动画结束后的样式则表现为由执行期间遇到的最后一个关键帧计算值(也就是停在最后一帧)。

  1. animation-fill-mode: both 表现如图:

一句话总结,综合了 animation-fill-mode: backwardsanimation-fill-mode: forwards 的设定。动画开始前的样式为动画运行时的第一帧,动画结束后停在最后一帧。

animation-iteration-count/animation-direction 动画循环次数和方向

讲到了 animation-fill-mode,我们就可以顺带讲讲这个两个比较好理解的属性 -- animation-iteration-countanimation-direction

  • animation-iteration-count 控制动画运行的次数,可以是数字或者 infinite,注意,数字可以是小数
  • animation-direction 控制动画的方向,正向、反向、正向交替与反向交替

在上面讲述 animation-fill-mode 时,我使用了动画运行时的第一帧替代了@Keyframes 中定义的第一帧这种说法,因为动画运行的第一帧和最后一帧的实际状态还会受到动画运行方向 animation-directionanimation-iteration-count 的影响。

在 CSS 动画中,由 animation-iteration-countanimation-direction 共同决定动画运行时的第一帧和最后一帧的状态。

  1. 动画运行的第一帧由 animation-direction 决定
  2. 动画运行的最后一帧由 animation-iteration-countanimation-direction 决定

动画的最后一帧,也就是动画运行的最终状态,并且我们可以利用 animation-fill-mode: forwards 让动画在结束后停留在这一帧,这个还是比较好理解的,但是 animation-fill-mode: backwardsanimation-direction 的关系很容易弄不清楚,这里简答讲解下。

设置一个 100px x 100px 的滑块,在一个 400px x 100px 的容器中,其代码如下:

<div class="g-father">
    <div class="g-box"></div>
</div>
.g-father {
    width: 400px;
    height: 100px;
    border: 1px solid #000;
}
.g-box {
    width: 100px;
    height: 100px;
    background: #333;
}

表现如下:

那么,加入 animation 之后,在不同的 animation-iteration-countanimation-direction 作用下,动画的初始和结束状态都不一样。

如果设置了 animation-fill-mode: backwards,则元素在动画未开始前的状态由 animation-direction 决定:

.g-box {
    ...
    animation: move 4s linear;
    animation-play-state: paused;
    transform: translate(0, 0);
}
@keyframes move {
    0% {
        transform: translate(100px, 0);
    }
    100% {
        transform: translate(300px, 0);
    }
}

注意这里 CSS 规则中,元素没有设置位移 transform: translate(0, 0),而在动画中,第一个关键帧和最后一个关键的 translateX 分别是 100px300px,配合不同的 animation-direction 初始状态如下。

下图假设我们设置了动画默认是暂停的 -- animation-play-state: paused,那么动画在开始前的状态为:

动画的分治与复用

讲完了每一个属性,我们再来看看一些动画使用过程中的细节。

看这样一个动画:

<div></div>
div {
    width: 100px;
    height: 100px;
    background: #000;
    animation: combine 2s;
}
@keyframes combine {
    100% {
        transform: translate(0, 150px);
        opacity: 0;
    }
}

这里我们实现了一个 div 块下落动画,下落的同时产生透明度的变化:

对于这样一个多个属性变化的动画,它其实等价于:

div {
    animation: falldown 2s, fadeIn 2s;
}

@keyframes falldown {
    100% {
        transform: translate(0, 150px);
    }
}
@keyframes fadeIn {
    100% {
        opacity: 0;
    }
}

在 CSS 动画规则中,animation 是可以接收多个动画的,这样做的目的不仅仅只是为了复用,同时也是为了分治,我们对每一个属性层面的动画能够有着更为精确的控制。

keyframes 规则的设定

我们经常能够在各种不同的 CSS 代码见到如下两种 CSS @keyframes 的设定:

  1. 使用百分比
@keyframes fadeIn {
    0% {
        opacity: 1;
    }
    100% {
        opacity: 0;
    }
}
  1. 使用 fromto
@keyframes fadeIn {
    from {
        opacity: 1;
    }
    to {
        opacity: 0;
    }
}

在 CSS 动画 @keyframes 的定义中,from 等同于 0%,而 to 等同于 100%

当然,当我们的关键帧不止 2 帧的时,更推荐使用百分比定义的方式。

除此之外,当动画的起始帧等同于 CSS 规则中赋予的值并且没有设定 animation-fill-mode0%from 这一帧是可以删除的。

动画状态的高优先级性

我曾经在这篇文章中 -- 深入理解 CSS(Cascading Style Sheets)中的层叠(Cascading) 讲过一个很有意思的 CSS 现象。

这也是很多人对 CSS 优先级的一个认知误区,在 CSS 中,优先级还需要考虑选择器的层叠(级联)顺序

只有在层叠顺序相等时,使用哪个值才取决于样式的优先级。

那什么是层叠顺序呢?

根据 CSS Cascading 4 最新标准:

CSS Cascading and Inheritance Level 5(Current Work)

定义的当前规范下申明的层叠顺序优先级如下(越往下的优先级越高,下面的规则按升序排列):

  • Normal user agent declarations
  • Normal user declarations
  • Normal author declarations
  • Animation declarations
  • Important author declarations
  • Important user declarations
  • Important user agent declarations
  • Transition declarations

简单翻译一下:

按照上述算法,大概是这样:

过渡动画过程中每一帧的样式 > 用户代理、用户、页面作者设置的!important样式 > 动画过程中每一帧的样式优先级 > 页面作者、用户、用户代理普通样式。

然而,经过多个浏览器的测试,实际上并不是这样。(尴尬了)

举个例子,我们可以通过这个特性,覆盖掉行内样式中的 !important 样式:

<p class="txt" style="color:red!important">123456789</p>
.txt {
    animation: colorGreen 2s infinite;
}
@keyframes colorGreen {
    0%,
    100% {
        color: green;
    }
}

在 Safari 浏览器下,上述 DEMO 文本的颜色为绿色,也就是说,处于动画状态中的样式,能够覆盖掉行内样式中的 !important 样式,属于最最高优先级的一种样式,我们可以通过无限动画、或者 animation-fill-mode: forwards,利用这个技巧,覆盖掉本来应该是优先级非常非常高的行内样式中的 !important 样式。

我在早两年的 Chrome 中也能得到同样的结果,但是到今天(2022-01-10),最新版的 Chrome 已经不支持动画过程中关键帧样式优先级覆盖行内样式 !important 的特性。

对于不同浏览器,感兴趣的同学可以利用我这个 DEMO 自行尝试,CodePen Demo - the priority of CSS Animation

CSS 动画的优化

这也是非常多人非常关心的一个重点。

我的 CSS 动画很卡,我应该如何去优化它?

动画元素生成独立的 GraphicsLayer,强制开始 GPU 加速

CSS 动画很卡,其实是一个现象描述,它的本质其实是在动画过程中,浏览器刷新渲染页面的帧率过低。通常而言,目前大多数浏览器刷新率为 60 次/秒,所以通常来讲 FPS 为 60 frame/s 时动画效果较好,也就是每帧的消耗时间为 16.67ms。

页面处于动画变化时,当帧率低于一定数值时,我们就感觉到页面的卡顿。

而造成帧率低的原因就是浏览器在一帧之间处理的事情太多了,超过了 16.67ms,要优化每一帧的时间,又需要完整地知道浏览器在每一帧干了什么,这个就又涉及到了老生常谈的浏览器渲染页面。

到今天,虽然不同浏览器的渲染过程不完全相同,但是基本上大同小异,基本上都是:

简化一下也就是这个图:

这两张图,你可以在非常多不同的文章中看到。

回归本文的重点,Web 动画很大一部分开销在于层的重绘,以层为基础的复合模型对渲染性能有着深远的影响。当不需要绘制时,复合操作的开销可以忽略不计,因此在试着调试渲染性能问题时,首要目标就是要避免层的重绘。那么这就给动画的性能优化提供了方向,减少元素的重绘与回流

这其中,如何减少页面的回流与重绘呢,这里就会运用到我们常说的** GPU 加速**。

GPU 加速的本质其实是减少浏览器渲染页面每一帧过程中的 reflow 和 repaint,其根本,就是让需要进行动画的元素,生成自己的 GraphicsLayer

浏览器渲染一个页面时,它使用了许多没有暴露给开发者的中间表现形式,其中最重要的结构便是层(layer)。

在 Chrome 中,存在有不同类型的层: RenderLayer(负责 DOM 子树),GraphicsLayer(负责 RenderLayer 的子树)。

GraphicsLayer ,它对于我们的 Web 动画而言非常重要,通常,Chrome 会将一个层的内容在作为纹理上传到 GPU 前先绘制(paint)进一个位图中。如果内容不会改变,那么就没有必要重绘(repaint)层。

而当元素生成了自己的 GraphicsLayer 之后,在动画过程中,Chrome 并不会始终重绘整个层,它会尝试智能地去重绘 DOM 中失效的部分,也就是发生动画的部分,在 Composite 之前,页面是处于一种分层状态,借助 GPU,浏览器仅仅在每一帧对生成了自己独立 GraphicsLayer 元素层进行重绘,如此,大大的降低了整个页面重排重绘的开销,提升了页面渲染的效率。

因此,CSS 动画(Web 动画同理)优化的第一条准则就是让需要动画的元素生成了自己独立的 GraphicsLayer,强制开始 GPU 加速,而我们需要知道是,GPU 加速的本质是利用让元素生成了自己独立的 GraphicsLayer,降低了页面在渲染过程中重绘重排的开销。

当然,生成自己的独立的 GraphicsLayer,不仅仅只有 transform3d api,还有非常多的方式。在 CSS 中,包括但不限于(找了很多文档,没有很全面的,需要一个一个去尝试,通过开启 Chrome 的 Layer border 选项):

  • 3D 或透视变换(perspective、transform) CSS 属性
  • 使用加速视频解码的
  • 拥有 3D (WebGL) 上下文或加速的 2D 上下文的 元素
  • 混合插件(如 Flash)
  • 对自己的 opacity 做 CSS 动画或使用一个动画变换的元素
  • 拥有加速 CSS 过滤器的元素
  • 元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
  • 元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

对于上述一大段非常绕的内容,你可以再看看这几篇文章:

除了上述准则之外,还有一些提升 CSS 动画性能的建议:

减少使用耗性能样式

不同样式在消耗性能方面是不同的,改变一些属性的开销比改变其他属性要多,因此更可能使动画卡顿。

例如,与改变元素的文本颜色相比,改变元素的 box-shadow 将需要开销大很多的绘图操作。box-shadow 属性,从渲染角度来讲十分耗性能,原因就是与其他样式相比,它们的绘制代码执行时间过长。这就是说,如果一个耗性能严重的样式经常需要重绘,那么你就会遇到性能问题。

类似的还有 CSS 3D 变换、mix-blend-modefilter,这些样式相比其他一些简单的操作,会更加的消耗性能。我们应该尽可能的在动画过程中降低其使用的频率或者寻找替代方案。

当然,没有不变的事情,在今天性能很差的样式,可能明天就被优化,并且浏览器之间也存在差异。

因此关键在于,我们需要针对每一起卡顿的例子,借助开发工具来分辨出性能瓶颈所在,然后设法减少浏览器的工作量。学会 Chrome 开发者工具的 Performance 面板及其他渲染相关的面板非常重要,当然这不是本文的重点。大家可以自行探索。

使用 will-change 提高页面滚动、动画等渲染性能

will-change 为 Web 开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。 这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏。

值得注意的是,用好这个属性并不是很容易:

  • 不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与 will-change 结合在一起的话,有可能会消耗很多机器资源,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。

  • 有节制地使用:通常,当元素恢复到初始状态时,浏览器会丢弃掉之前做的优化工作。但是如果直接在样式表中显式声明了 will-change 属性,则表示目标元素可能会经常变化,浏览器会将优化工作保存得比之前更久。所以最佳实践是当元素变化之前和之后通过脚本来切换 will-change 的值。

  • 不要过早应用 will-change 优化:如果你的页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。 will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。过度使用 will-change 会导致大量的内存占用,并会导致更复杂的渲染过程,因为浏览器会试图准备可能存在的变化过程。这会导致更严重的性能问题。

  • 给它足够的工作时间:这个属性是用来让页面开发者告知浏览器哪些属性可能会变化的。然后浏览器可以选择在变化发生前提前去做一些优化工作。所以给浏览器一点时间去真正做这些优化工作是非常重要的。使用时需要尝试去找到一些方法提前一定时间获知元素可能发生的变化,然后为它加上 will-change 属性。

有人说 will-change 是良药,也有人说是毒药,在具体使用的时候,可以多测试一下。

最后

好了,本文从多个方面,由浅入深地描述了 CSS 动画我认为的一些比较重要、值得一讲、需要注意的点。当然很多地方点到即止,或者限于篇幅没有完全展开,很多细节还需要读者进一步阅读规范或者自行尝试验证,实践出真知,纸上得来终觉浅。

OK,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

本文将介绍 CSS 中一个非常有意思的属性 mask 。

顾名思义,mask 译为遮罩。在 CSS 中,mask 属性允许使用者通过遮罩或者裁切特定区域的图片的方式来隐藏一个元素的部分或者全部可见区域。

其实 mask 的出现已经有一段时间了,只是没有特别多实用的场景,在实战中使用的非常少,本文将罗列一些使用 mask 创造出来的有意思的场景。

语法

最基本,使用 mask 的方式是借助图片,类似这样:

{
    /* Image values */
    mask: url(mask.png);                       /* 使用位图来做遮罩 */
    mask: url(masks.svg#star);                 /* 使用 SVG 图形中的形状来做遮罩 */
}

当然,使用图片的方式后文会再讲。借助图片的方式其实比较繁琐,因为我们首先还得准备相应的图片素材,除了图片,mask 还可以接受一个类似 background 的参数,也就是渐变。

类似如下使用方法:

{
    mask: linear-gradient(#000, transparent)                      /* 使用渐变来做遮罩 */
}

那该具体怎么使用呢?一个非常简单的例子,上述我们创造了一个从黑色到透明渐变色,我们将它运用到实际中,代码类似这样:

下面这样一张图片,叠加上一个从透明到黑色的渐变,

{
    background: url(image.png) ;
    mask: linear-gradient(90deg, transparent, #000);
}

应用了 mask 之后,就会变成这样:

image

这个 DEMO,可以先简单了解到 mask 的基本用法。

这里得到了使用 mask 最重要结论:图片与 mask 生成的渐变的 transparent 的重叠部分,将会变得透明。

值得注意的是,上面的渐变使用的是 linear-gradient(90deg, transparent, #fff),这里的 #fff 纯色部分其实换成任意颜色都可以,不影响效果。

CodePen Demo -- 使用 MASK 的基本使用

使用 MASK 进行图片裁切

利用上述简单的运用,我们可以使用 mask 实现简单的图片裁剪。

使用 mask 实现图片切角遮罩

使用线性渐变,我们实现一个简单的切角图形:

.notching{
    width: 200px;
    height: 120px;
    background:
    linear-gradient(135deg, transparent 15px, deeppink 0)
    top left,
    linear-gradient(-135deg, transparent 15px, deeppink 0)
    top right,
    linear-gradient(-45deg, transparent 15px, deeppink 0)
    bottom right,
    linear-gradient(45deg, transparent 15px, deeppink 0)
    bottom left;
    background-size: 50% 50%;
    background-repeat: no-repeat;
}

像是这样:

image

我们将上述渐变运用到 mask 之上,而 background 替换成一张图片,就可以得到运用了切角效果的图片:

    background: url(image.png);
    mask:
        linear-gradient(135deg, transparent 15px, #fff 0)
        top left,
        linear-gradient(-135deg, transparent 15px, #fff 0)
        top right,
        linear-gradient(-45deg, transparent 15px, #fff 0)
        bottom right,
        linear-gradient(45deg, transparent 15px, #fff 0)
        bottom left;
    mask-size: 50% 50%;
    mask-repeat: no-repeat;

得到的效果如下:

image

CodePen Demo -- 使用 MASK 实现图片切角遮罩

当然,实现上述效果还有其他很多种方式,譬如 clip-path,这里的 mask 也是一种方式。

多张图片下使用 mask

上述是单张图片使用 mask 的效果。下面我们看看多张图片下,使用 mask 能碰撞出什么样的火花。

假设我们有两张图片,使用 mask,可以很好将他们叠加在一起进行展示。最常见的一个用法:

div {
    position: relative;
    background: url(image1.jpg);

    &::before {
        position: absolute;
        content: "";
        top: 0;left: 0; right: 0;bottom: 0;
        background: url(image2.jpg);
        mask: linear-gradient(45deg, #000 50%, transparent 50%);
    }
}

两张图片,一张完全重叠在另外一张之上,然后使用 mask: linear-gradient(45deg, #000 50%, transparent 50%) 分割两张图片:

image

CodePen Demo -- MASK 的基本使用,多张图片下的基本用法

当然,注意上面我们使用的 mask 的渐变,是完全的实色变化,没有过度效果。

我们稍微修改一下 mask 内的渐变:

{
- mask: linear-gradient(45deg, #000 50%, transparent 50%)
+ mask: linear-gradient(45deg, #000 40%, transparent 60%)
}

即可得到图片1向图片2过渡切换的效果:

image

CodePen Demo -- MASK 的基本使用,多张图片下的基本用法2

使用 MASK 进行转场动画

有了上面的铺垫。运用上面的介绍的一些方法,我们就可以使用 mask 来进行一些图片切换间的转场动画。

使用线性渐变 mask:linear-gradient() 进行切换

还是上面的 Demo,我们通过动态的去改变 mask 的值来实现图片的显示/转场效果。

代码可能是这样:

div {
    background: url(image1.jpg);
    animation: maskMove 2s linear;
}

@keyframes {
  0% {
    mask: linear-gradient(45deg, #000 0%, transparent 5%, transparent 5%);
  }
  1% {
    mask: linear-gradient(45deg, #000 1%, transparent 6%, transparent 6%);
  }
  ...
  100% {
    mask: linear-gradient(45deg, #000 100%, transparent 105%, transparent 105%);
  }
}

当然,像上面那样一个一个写,会比较费力,通常我们会借助 SASS/LESS 等预处理器进行操作。像是这样:

div {
    position: relative;
    background: url(image2.jpg) no-repeat;

    &::before {
        position: absolute;
        content: "";
        top: 0;left: 0; right: 0;bottom: 0;
        background: url(image1.jpg);
        animation: maskRotate 1.2s ease-in-out;
    }
}

@keyframes maskRotate {
    @for $i from 0 through 100 { 
        #{$i}% {
            mask: linear-gradient(45deg, #000 #{$i + '%'}, transparent #{$i + 5 + '%'}, transparent 1%);
        }
    }
}

可以得到下面这样的效果(单张图片的显隐及两张图片下的切换):

mask1
mask2
CodePen Demo -- MASK linear-gradient 转场

使用角向渐变 mask: conic-gradient() 进行切换

当然,除了 mask: linear-gradient(),使用径向渐变或者角向渐变也都是可以的。使用角向渐变的原理也是一样的:

@keyframes maskRotate {
    @for $i from 0 through 100 { 
        #{$i}% {
            mask: conic-gradient(#000 #{$i - 10 + '%'}, transparent #{$i + '%'}, transparent);
        }
    }
}

可以实现图片的角向渐显/切换:

mask3

CodePen Demo -- MASK conic-gradient 转场

这个技巧,在张鑫旭的这篇文章里,有更多丰富的例子,可以移步阅读:

你用的那些CSS转场动画可以换一换了

运用这个技巧,我们就可以实现很多有意思的图片效果。像是这样:

mask4

mask 碰撞滤镜与混合模式

继续下一环节。CSS 中很多有意思的属性,和滤镜和混合模式一结合,会碰撞出更多火花。

mask & 滤镜 filter: contrast()

首先,我们利用多重径向渐变,实现这样一张图。

{
  background: radial-gradient(#000, transparent);
  background-size: 20px 20px;
}

image

看着没什么特别,我们利用 filter: contrast() 对比度滤镜,改造一下。代码大概是这样:

html,body {
    width: 100%;
    height: 100%;
    filter: contrast(5);
}

div {
    position: relative;
    width: 100%;
    height: 100%;
    background: #fff;
    
    &::before {
        content: "";
        position: absolute;
        top: 0; right: 0; bottom: 0; left: 0;
        background: radial-gradient(#000, transparent);
        background-size: 20px 20px;
    }
}

即可得到这样的图形,利用对比度滤镜,将图形变得非常的锐化。

image

这个时候,我们再叠加上不同的 mask 遮罩。即可得到各种有意思的图形效果。

body {
    filter: contrast(5);
}

div {
    position: relative;
    background: #fff;
    
    &::before {
        background: radial-gradient(#000, transparent);
        background-size: 20px 20px;
      + mask: linear-gradient(-180deg, rgba(255, 255, 255, 1), rgba(255, 255, 255, .5));
    }
}

image

CodePen Demo -- 使用 mask 搭配滤镜 contrast

我们叠加了一个线性渐变的 mask linear-gradient(-180deg, rgba(255, 255, 255, 1), rgba(255, 255, 255, .5)),注意,两个渐变颜色都是带透明度的。

或者换一个径向渐变:

{
    mask: repeating-radial-gradient(circle at 35% 65%, #000, rgba(0, 0, 0, .5), #000 25%);
}

image

CodePen Demo -- 使用 mask 搭配滤镜 contrast

好的,下一步,与上文类似,我们添加上动画。

div {
    ...
    
    &::before {
        background: radial-gradient(#000, transparent);
        background-size: 20px 20px;
        mask: repeating-radial-gradient(circle at 35% 65%, #000, rgba(0, 0, 0, .5), #000 25%);
        animation: maskMove 15s infinite linear;
    }
}

@keyframes maskMove {
    @for $i from 0 through 100 { 
        #{$i}% {
            mask: repeating-radial-gradient(circle at 35% 65%, #000, rgba(0, 0, 0, .5), #000 #{$i + 10 +  '%'});
        }
    }
}

看看,可以得到了非常酷炫的动画效果:

mask5

CodePen Demo -- 使用 mask 搭配滤镜 contrast 及动画

还记得使用 filter: hue-rotate() 色相滤镜吗。再加上它,我们可以让颜色也变化起来。

mask6

CodePen Demo -- 使用 mask 搭配滤镜 contrast 及动画2

mask & 滤镜 filter: contrast() & 混合模式

接下来我们再叠加上混合模式。

注意到上面,其实我们的容器背景色是白色 #fff

我们可以通过多嵌套一层层级,再增加一个容器背景色,再叠加上混合模式,产生不一样的效果。

先不添加使用 mask,重新构造一下结构,最终的伪代码带个是这样:

<div class="wrap">
    <div class="inner"></div>
</div>
.wrap {
    position: relative;
    height: 100%;
    background: linear-gradient(45deg, #f44336, #ff9800, #ffeb3b, #8bc34a, #00bcd4, #673ab7);
}

.inner {
    height: 100%;
    background: #000;
    filter: contrast(700%);
    mix-blend-mode: multiply;
    
    &::before {
        content: "";
        position: absolute;
        top: 0; right: 0; bottom: 0; left: 0;
        background: radial-gradient(#fff, transparent);
        background-size: 12px 12px;
    }
}

原理示例图如下:

image

我们就可以得到如下的效果:

image

OK,到这一步,mask 还没有运用上,我们再添加上 mask。

.wrap {
    background: linear-gradient(45deg, #f44336, #ff9800, #ffeb3b, #8bc34a, #00bcd4, #673ab7);
}

.inner {
    ...
    filter: contrast(700%);
    mix-blend-mode: multiply;
    
    &::before {
        background: radial-gradient(#fff, transparent);
        background-size: 12px 12px;
      + mask: linear-gradient(#000, rgba(0, 0, 0, .5));
    }
}

image

CodePen Demo -- mask & filter & blend-mode

实际效果比截图好很多,可以点击 Demo 去看看。

当然,这里叠加的是 mix-blend-mode: multiply ,可以尝试其他混合模式,得到其他不一样的效果。

譬如,叠加 mix-blend-mode: difference,等等等等:

image

更多有意思的叠加,感兴趣的同学需要自己多加尝试。

mask 与图片

当然,mask 最本质的作用应该还是作用于图片。上面得到的重要结论:

图片与 mask 生成的渐变的 transparent 的重叠部分,将会变得透明。

也可以作用于 mask 属性传入的图片。也就是说,mask 是可以传入图片素材的,并且遵循 background-image 与 mask 图片的透明重叠部分,将会变得透明。

运用这个技巧,可以制作非常酷炫的转场动画:

mask7

这里其实主要是在 mask 中运用了这样一张图片:

image

然后,使用了逐帧动画,快速切换每一帧的 mask :

.img1 {
    background: url(image1.jpg) no-repeat left top;
}

.img2 {
    mask: url(https://i.imgur.com/AYJuRke.png);
    mask-size: 3000% 100%;
    animation: maskMove 2s steps(29) infinite;
}

.img2::before {
    background: url(image2.jpg) no-repeat left top;
}

@keyframes maskMove {
    from {
        mask-position: 0 0;
    }
    to {
        mask-position: 100% 0;
    }
}

CodePen Demo -- mask 制作转场动画

当然,这个也是可以加上各种动画的。上面已经演示了很多次了,感兴趣的同学可以自己尝试尝试。

最后

说了这么多,mask 其实还是属于一个比较冷门的属性。在日常业务中能运用上的机会不多。

而且兼容性不算特别好,打开 MDN,可以看到,除了 mask 本身,还有很多与 mask 相关的属性,只是目前大部分还属于实验室阶段。本文只是初略的介绍了 mask 本身,对 mask 相关的一些属性将会另起一文。

image

当然,即便如此,从属性本身而言,我觉得 mask 还是非常有意思的,带来了 CSS 更多可能性。


好了,本文到此结束,希望对你有帮助 :)

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

今天在论坛,有看到这样一道非常有意思的题目,简单的代码如下:

<div>
    <p id="a">First Paragraph</p>
</div>

样式如下:

p#a {
    color: green;
}
div::first-line {
    color: blue;
}

试问,标签 <p> 内的文字的颜色,是 green 还是 blue 呢?

有趣的是,这里的最终结果是蓝色,也就是 color: blue 生效了。

不对,正常而言,ID 选择器的优先级不应该比伪类选择器高么?为什么这里反而是伪类选择器的优先级更高呢?

并且,打开调试模式,我们定位到 <p> 元素上,只看到了 color: green 生效,没找到 div::first-line 的样式定义:

只有再向上一层,我们找到 <div> 的样式规则,才能在最下面看到这样一条规则:

因此,这里很明显,是**<p> 标签继承了父元素 <div> 的这条规则,并且作用到了自身第一行元素之上,覆盖了原本的 ID 选择器内定义的 color: green**。

再进行验证

这里,另外一个比较迷惑的点在于,为什么 ID 选择器的优先级比 ::first-line 选择器更低。

我们再做一些简单的尝试:

下面的 DEMO 展示了 ::first-line 样式和各种选择器共同作用时的优先级对比,甚至包括了 !important 规则:

  • 第 1 段通过标签选择器设置为灰色
  • 第 2 段通过类选择器设置为灰色
  • 第 3 段通过 ID 选择器设置为灰色
  • 第 4 段通过 !important bash 设置为灰色

综上的同时,每一段我们同时都使用了 ::first-line 选择器。

<h2>::first-line vs. tag selector</h2>
<p>This paragraph ...</p>  

<h2>::first-line vs class selector</h2>
<p class="p2">This paragraph color i...</p>  

<h2>::first-line vs ID selector</h2>
<p id="p3">This paragraph color is set ...</p>  

<h2>::first-line vs !important</h2>
<p id="p4">This paragraph color is ....</p>  
p {
  color: #444;
}
p::first-line {
  color: deepskyblue;
}

.p2 {
  color: #444;
}
.p2::first-line {
  color: tomato;
}

#p3 {
  color: #444;
}
#p3::first-line {
  color: firebrick;
}

#p4 {
  color: #444 !important;
}
#p4::first-line {
  color: hotpink;
}

CodePen Demo -- ::first-line: demo

看看效果:

可以看到,无论是什么选择器,优先级都没有 ::first-line 高。

究其原因,在于,::first-line 其实是个伪元素而不是一个伪类,被其选中的内容其实会被当成元素的子元素进行处理,类似于 ::before::after 一样,因此,对于父元素的 color 规则,对于它而言只是一种级联关系,通过 ::first-line 本身定义的规则,优先级会更高!

这也是为什么,在 MDN 文档中,更推荐的是双冒号的写法(当然浏览器都支持单冒号的写法)-- MDN -- ::first-line

再来一题,MDN 的错误例子?一个有意思的现象

说完上面这题。我们再来看看一题,非常类似的题目。

在 MDN 介绍 :not 的页面,有这样一个例子:

/* Selects any element that is NOT a paragraph */
:not(p) {
  color: blue;
}

意思是,:not(p) 可以选择任何不是 <p> 标签的元素。然而,上面的 CSS 选择器,在如下的 HTML 结构,实测的结果不太对劲。

<p>p</p>
<div>div</div>
<span>span</span>
<h1>h1</h1>

结果如下:

CodePen Demo -- :not pesudo demo

意思是,:not(p) 仍然可以选中 <p> 元素。是的,在多个浏览器,得到的效果都是一致的。

看到这里,你可以再停一下,思考一下,为什么 <p> 元素的颜色仍旧是 color: blue

这是为什么呢?解答一下:

这是由于 :not(p) 同样能够选中 <body>,那么 <body> 的 color 即变成了 blue,由于 color 是一个可继承属性,<p> 标签继承了 <body> 的 color 属性,导致看到的 <p> 也是蓝色。

我们把它改成一个不可继承的属性,试试看:

/* Selects any element that is NOT a paragraph */
:not(p) {
  border: 1px solid;
}

OK,这次 <p> 没有边框体现,没有问题!

因此,实际使用的时候,需要一定要注意样式继承的问题!

最后

本文到此结束,希望对你有帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

循序渐进,看看只使用 CSS ,可以鼓捣出什么样的充电动画效果。

画个电池

当然,电池充电,首先得用 CSS 画一个电池,这个不难,随便整一个:

image

欧了,勉强就是它了。有了电池,那接下来直接充电吧。最最简单的动画,那应该是用色彩把整个电池灌满即可。

方法很多,代码也很简单,直接看效果:

charging1

有内味了,如果要求不高,这个勉强也就能够交差了。通过蓝色渐变表示电量,通过色块的位移动画实现充电的动画。但是总感觉少了点什么。

增加阴影及颜色的变化

如果要继续优化的话,需要添加点细节。

我们知道,低电量时,电量通常表示为红色,高电量时表示为绿色。再给整个色块添加点阴影的变化,呼吸的感觉,让充电的效果看起来确实是在动。

charging2

知识点

到这里,其实只有一个知识点:

  • 使用 filter: hue-rotate() 对渐变色彩进行色彩过渡变换动画

我们无法对一个渐变色直接进行 animation ,这里通过滤镜对色相进行调整,从而实现了渐变色的变换动画。

上述例子完整的 Demo: CodePen Demo -- Battery Animation One

添加波浪

ok,刚刚算一个小里程碑,接下来再进一步。电量的顶部为一条直线有点呆呆的感觉,这里我们进行改造一下,如果能将顶部直线,改为波浪滚动,效果会更为逼真一点。

改造之后的效果:

charging3

使用 CSS 实现这种波浪滚动效果,其实只是用了一种障眼法,具体的可以我早期写的这篇文章:

纯 CSS 实现波浪效果!

知识点

这里的一个知识点就是上述说的使用 CSS 实现简易的波浪效果,通过障眼法实现,看看图就明白了:

charging4

上述例子完整的 Demo: CodePen Demo -- Battery Animation Two

OK,到这,上述效果加上数字变化已经算是一个比较不错的效果了。当然上面的效果看上去还是很 CSS 的,就是一眼看到就觉得用 CSS 是可以做到的。

使用强大的 CSS 滤镜实现安卓充电动画效果

那下面这个呢?

image

用安卓手机的同学肯定不陌生,这个是安卓手机在充电的时候的效果。看到这个我就很好奇,使用 CSS 能做到吗?

经过一番尝试,发现使用 CSS 也是可以很好的模拟这种动画效果:

charging5

上述 Gif 录制的效果图是完全使用 CSS 模拟的效果。

上述例子完整的 Demo: HuaWei Battery Charging Animation

知识点

拆解一下知识点,最主要的其实是用到了 filter: contrast() 以及 filter: blur() 这两个滤镜,可以很好的实现这种融合效果。

单独将两个滤镜拿出来,它们的作用分别是:

  1. filter: blur(): 给图像设置高斯模糊效果。
  2. filter: contrast(): 调整图像的对比度。

但是,当他们“合体”的时候,产生了奇妙的融合现象。

先来看一个简单的例子:

filtermix

仔细看两圆相交的过程,在边与边接触的时候,会产生一种边界融合的效果,通过对比度滤镜把高斯模糊的模糊边缘给干掉,利用高斯模糊实现融合效果。

当然,这种效果在之前的文章也多次提及过,更具体的,可以看看:

颜色的变换

当然,这里也是可以加上颜色的变换,效果也很不错:

charging6

上述例子完整的 Demo: HuaWei Battery Charging Animation

容易忽视的点

通过调节 filter: blur()filter: contrast() 属性的值,动画效果其实会有很大程度的变化,好的效果需要不断的调试。当然,经验在其中也是发挥了很重要的作用,说到底还是要多尝试。

最后

本文给出的几个充电动画,效果渐进增强,本文只指出了最核心的知识点。但是在实际输出的过程中有很多小细节是本文没有提及的,感兴趣的同学还是应该点进 Demo 好好看看源码或者自己动手实现一遍。

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

好了,本文到此结束,希望对你有帮助 :)

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

在 CSS 选择器家族中,新增这样一类比较新的选择器 -- 逻辑选择器,目前共有 4 名成员:

  • :is
  • :where
  • :not
  • :has

本文将带领大家了解、深入它们。做到学以致用,写出更现代化的选择器。


:is 伪类选择器

:is() CSS伪类函数将选择器列表作为参数,并选择该列表中任意一个选择器可以选择的元素。

在之前,对于多个不同父容器的同个子元素的一些共性样式设置,可能会出现如下 CSS 代码:

header p:hover,
main p:hover,
footer p:hover {
  color: red;
  cursor: pointer;
}

而如今有了 :is() 伪类,上述代码可以改写成:

:is(header, main, footer) p:hover {
  color: red;
  cursor: pointer;
}

它并没有实现某种选择器的新功能,更像是一种语法糖,类似于 JavaScript ES6 中的 Class() 语法,只是对原有功能的重新封装设计,实现了更容易的表达一个操作的语法,简化了某些复杂代码的写法。

语法糖(syntactic sugar)是指编程语言中可以更容易的表达一个操作的语法,它可以使程序员更加容易去使用这门语言,操作可以变得更加清晰、方便,或者更加符合程序员的编程习惯。用比较通俗易懂的方式去理解就是,在之前的某个语法的基础上改变了一种写法,实现的功能相同,但是写法不同了,主要是为了让开发人员在使用过程中更方便易懂。

一图胜前言(引用至 New CSS functional pseudo-class selectors :is() and :where()):

支持多层层叠连用

再来看看这种情况,原本的 CSS 代码如下:

<div><i>div i</i></div>
<p><i>p i</i></p>
<div><span>div span</span></div>
<p><span>p span</span></p>
<h1><span>h1 span</span></h1>
<h1><i>h1 i</i></h1>

如果要将上述 HTML 中,<div><p> 下的 <span><i> 的 color 设置为 red,正常的 CSS 可能是这样:

div span,
div i,
p span,
p i {
    color: red;
}

有了 :is() 后,代码可以简化为:

:is(div, p) :is(span, i) {
    color: red;
}

结果如下:

这里,也支持 :is() 的层叠连用。通过 :is(div, p) :is(span, i) 的排列组合,可以组合出上述 4 行的选择器,达到同样的效果。

当然,这个例子比较简单,看不出 :is() 的威力。下面这个例子就比较明显,这么一大段 CSS 选择器代码:

ol ol ul,     ol ul ul,     ol menu ul,     ol dir ul,
ol ol menu,   ol ul menu,   ol menu menu,   ol dir menu,
ol ol dir,    ol ul dir,    ol menu dir,    ol dir dir,
ul ol ul,     ul ul ul,     ul menu ul,     ul dir ul,
ul ol menu,   ul ul menu,   ul menu menu,   ul dir menu,
ul ol dir,    ul ul dir,    ul menu dir,    ul dir dir,
menu ol ul,   menu ul ul,   menu menu ul,   menu dir ul,
menu ol menu, menu ul menu, menu menu menu, menu dir menu,
menu ol dir,  menu ul dir,  menu menu dir,  menu dir dir,
dir ol ul,    dir ul ul,    dir menu ul,    dir dir ul,
dir ol menu,  dir ul menu,  dir menu menu,  dir dir menu,
dir ol dir,   dir ul dir,   dir menu dir,   dir dir dir {
  list-style-type: square;
}

可以利用 :is() 优化为:

:is(ol, ul, menu, dir) :is(ol, ul, menu, dir) :is(ul, menu, dir) {
  list-style-type: square;
}

不支持伪元素

有个特例,不能用 :is() 来选取 ::before::after 两个伪元素。譬如:

注意,仅仅是不支持伪元素,伪类,譬如 :focus:hover 是支持的。

div p::before,
div p::after {
    content: "";
    //...
}

不能写成:

div p:is(::before, ::after) {
    content: "";
    //...
}

:is 选择器的优先级

看这样一种有意思的情况:

<div>
    <p class="test-class" id="test-id">where & is test</p>
</div>
<div>
    <p class="test-class">where & is test</p>
</div>

我们给带有 .test-class 的元素,设置一个默认的颜色:

div .test-class {
    color: red;
}

如果,这个时候,我们引入 :is() 进行匹配:

div :is(p) {
    color: blue;
}

此时,由于 div :is(p) 可以看成 div p,优先级是没有 div .test-class 高的,因此,被选中的文本的颜色是不会发生变化的。

但是,如果,我们在 :is() 选择器中,加上一个 #test-id,情况就不一样了。

div :is(p, #text-id) {
    color: blue;
}

按照理解,如果把上述选择器拆分,上述代码可以拆分成:

div p {
    color: blue;
}
div #text-id {
    color: blue;
}

那么,我们有理由猜想,带有 #text-id<p> 元素由于有了更高优先级的选择器,颜色将会变成 blue,而另外一个 div p 由于优先级不够高的问题,导致第一段文本依旧是 green

但是,这里,神奇的是,两段文本都变成了 blue

CodePen Demo -- the specificity of CSS :is selector

这是由于,:is() 的优先级是由它的选择器列表中优先级最高的选择器决定的。我们不能把它们割裂开来看。

对于 div :is(p, #text-id)is:() 内部有一个 id 选择器,因此,被该条规则匹配中的元素,全部都会应用 div #id 这一级别的选择器优先级。这里非常重要,再强调一下,对于 :is() 选择器的优先级,我们不能把它们割裂开来看,它们是一个整体,优先级取决于选择器列表中优先级最高的选择器

:is 的别名 :matches() 与 :any()

:is() 是最新的规范命名,在之前,有过有同样功能的选择,分别是:

:is(div, p) span {}
// 等同于
:-webkit-any(div, p) span {}
:-moz-any(div, p) span {}
:matches(div, p) span {}

当然,下面 3 个都已经废弃,不建议再继续使用。而到今天(2022-04-27):is() 的兼容性已经非常不错了,不需要兼容 IE 系列的话可以考虑开始用起来(配合 autoprefixer),看看 CanIUse

:where 伪类选择器

了解了 :is 后,我们可以再来看看 :where,它们两个有着非常强的关联性。:where 同样是将选择器列表作为其参数,并选择可以由该列表中的选择器之一选择的任何元素。

还是这个例子:

:where(header, main, footer) p:hover {
  color: red;
  cursor: pointer;
}

上述的代码使用了 :where,可以近似的看为:

header p:hover,
main p:hover,
footer p:hover {
  color: red;
  cursor: pointer;
}

这就有意思了,这不是和上面说的 :is 一样了么?

那么它们的区别在什么地方呢?

:is:where 的区别

首先,从语法上,:is:where 是一模一样的。它们的核心区别点在于 优先级

来看这样一个例子:

<div>
    <p>where & is test</p>
</div>

CSS 代码如下:

:is(div) p {
    color: red;
}
:where(div) p {
    color: green;
}

正常按我们的理解而言,:is(div) p:where(div) p 都可以转化为 div p,由于 :where(div) p 后定义,所以文字的颜色,应该是 green 绿色,但是,实际的颜色表现为 color: red 红色:

这是因为,:where():is() 的不同之处在于,:where() 的优先级总是为 0 ,但是 :is() 的优先级是由它的选择器列表中优先级最高的选择器决定的。

上述的例子还不是特别明显,我们再稍微改造下:

<div id="container">
    <p>where & is test</p>
</div>

我们给 div 添加上一个 id 属性,改造上述 CSS 代码:

:is(div) p {
    color: red;
}
:where(#container) p {
    color: green;
}

即便如此,由于 :where(#container) 的优先级为 0,因此文字的颜色,依旧为红色 red。:where() 的优先级总是为 0 这一点在使用的过程中需要牢记。

组合、嵌套

CSS 选择器的一个非常大的特点就在于组合嵌套。:is:where 也不例外,因此,它们也可以互相组合嵌套使用,下述的 CSS 选择器都是合理的:

/* 组合*/
:is(h1,h2) :where(.test-a, .test-b) {
  text-transform: uppercase;
}
/* 嵌套*/
.title:where(h1, h2, :is(.header, .footer)) {
  font-weight: bold;
}

这里简单总结下,:is:where 都是非常好的分组逻辑选择器,唯一的区别在于:where() 的优先级总是为 0,而:is() 的优先级是由它的选择器列表中优先级最高的选择器决定的。

:not 伪类选择器

下面我们介绍一下非常有用的 :not 伪类选择器。

:not 伪类选择器用来匹配不符合一组选择器的元素。由于它的作用是防止特定的元素被选中,它也被称为反选伪类(negation pseudo-class)。

举个例子,HTML 结构如下:

<div class="a">div.a</div>
<div class="b">div.b</div>
<div class="c">div.c</div>
<div class="d">div.d</div>
div:not(.b) {
    color: red;
}

div:not(.b) 它可以选择除了 class 为 .b 元素之外的所有 div 元素:

MDN 的错误例子?一个有意思的现象

有趣的是,在 MDN 介绍 :not 的页面,有这样一个例子:

/* Selects any element that is NOT a paragraph */
:not(p) {
  color: blue;
}

意思是,:not(p) 可以选择任何不是 <p> 标签的元素。然而,上面的 CSS 选择器,在如下的 HTML 结构,实测的结果不太对劲。

<p>p</p>
<div>div</div>
<span>span</span>
<h1>h1</h1>

结果如下:

意思是,:not(p) 仍然可以选中 <p> 元素。我尝试了多个浏览器,得到的效果都是一致的。

CodePen Demo -- :not pesudo demo

这是为什么呢?这是由于 :not(p) 同样能够选中 <body>,那么 <body> 的 color 即变成了 blue,由于 color 是一个可继承属性,<p> 标签继承了 <body> 的 color 属性,导致看到的 <p> 也是蓝色。

我们把它改成一个不可继承的属性,试试看:

/* Selects any element that is NOT a paragraph */
:not(p) {
  border: 1px solid;
}

OK,这次 <p> 没有边框体现,没有问题!实际使用的时候,需要注意这一层继承的问题!

:not 的优先级问题

下面是一些使用 :not 需要注意的问题。

:not:is:where 这几个伪类不像其它伪类,它不会增加选择器的优先级。它的优先级即为它参数选择器的优先级。

并且,在 CSS Selectors Level 3:not() 内只支持单个选择器,而从 CSS Selectors Level 4 开始,:not() 内部支持多个选择器,像是这样:

/* CSS Selectors Level 3,:not 内部如果有多个值需要分开 */
p:not(:first-of-type):not(.special) {
}
/* CSS Selectors Level 4 支持使用逗号分隔*/
p:not(:first-of-type, .special) {
}

:is() 类似,:not() 选择器本身不会影响选择器的优先级,它的优先级是由它的选择器列表中优先级最高的选择器决定的。

:not(*) 问题

使用 :not(*) 将匹配任何非元素的元素,因此这个规则将永远不会被应用。

相当于一段没有任何意义的代码。

:not() 不能嵌套 :not()

禁止套娃。:not 伪类不允许嵌套,这意味着 :not(:not(...)) 是无效的。

:not() 实战解析

那么,:not() 有什么特别有意思的应用场景呢?我这里列举一个。

W3 CSS selectors-4 规范 中,新增了一个非常有意思的 :focus-visible 伪类。

:focus-visible 这个选择器可以有效地根据用户的输入方式(鼠标 vs 键盘)展示不同形式的焦点。

有了这个伪类,就可以做到,当用户使用鼠标操作可聚焦元素时,不展示 :focus 样式或者让其表现较弱,而当用户使用键盘操作焦点时,利用 :focus-visible,让可获焦元素获得一个较强的表现样式。

看个简单的 Demo:

<button>Test 1</button>
button:active {
  background: #eee;
}
button:focus {
  outline: 2px solid red;
}

使用鼠标点击:

可以看到,使用鼠标点击的时候,触发了元素的 :active 伪类,也触发了 :focus伪类,不太美观。但是如果设置了 outline: none 又会使键盘用户的体验非常糟糕。因为当键盘用户使用 Tab 尝试切换焦点的时候,会因为 outline: none 而无所适从。

因此,可以使用 :focus-visible 伪类改造一下:

button:active {
  background: #eee;
}
button:focus {
  outline: 2px solid red;
}
button:focus:not(:focus-visible) {
  outline: none;
}

看看效果,分别是在鼠标点击 Button 和使用键盘控制焦点点击 Button:

CodePen Demo -- :focus-visible example

可以看到,使用鼠标点击,不会触发 :foucs,只有当键盘操作聚焦元素,使用 Tab 切换焦点时,outline: 2px solid red 这段代码才会生效。

这样,我们就既保证了正常用户的点击体验,也保证了无法使用鼠标的用户的焦点管理体验,在可访问性方面下了功夫。

值得注意的是,这里为什么使用了 button:focus:not(:focus-visible) 这么绕的写法而不是直接这样写呢:

button:focus {
  outline: unset;
}
button:focus-visible {
  outline: 2px solid red;
}

解释一下,button:focus:not(:focus-visible) 的意思是,button 元素触发 focus 状态,并且不是通过 focus-visible 触发,理解过来就是在支持 :focus-visible 的浏览器,通过鼠标激活 :focus 的 button 元素,这种情况下,不需要设置 outline

为的是兼容不支持 :focus-visible 的浏览器,当 :focus-visible 不兼容时,还是需要有 :focus 伪类的存在。

因此,这里借助 :not() 伪类,巧妙的实现了一个实用效果的方案降级。

这里有点绕,需要好好理解理解。

:not 兼容性

经历了 CSS Selectors Level 3 & CSS Selectors Level 4 两个版本,到今天(2020-05-04),除去 IE 系列,:not 的兼容性已经非常之好了:

:has 伪类选择器

OK。最后到所有逻辑选择器里面最重磅的 :has 出场了。它之所以重要是因为它的诞生,填补了在之前 CSS 选择器中,没有核心意义上真正的父选择器的空缺。

:has 伪类接受一个选择器组作为参数,该参数相对于该元素的 :scope 至少匹配一个元素。

实际看个例子:

<div>
    <p>div -- p</p>
</div>
<div>
    <p class="g-test-has">div -- p.has</p>
</div>
<div>
    <p>div -- p</p>
</div>
div:has(.g-test-has) {
    border: 1px solid #000;
} 

我们通过 div:has(.g-test-has) 选择器,意思是,选择 div 下存在 class 为 .g-test-has 的 div 元素。

注意,这里选择的不是 :has() 内包裹的选择器选中的元素,而是使用 :has() 伪类的宿主元素。

效果如下:

可以看到,由于第二个 div 下存在 class 为 .g-test-has 的元素,因此第二个 div 被加上了 border。

:has() 父选择器 -- 嵌套结构的父元素选择

我们再通过几个 DEMO 加深下印象。:has() 内还可以写的更为复杂一点。

<div>
    <span>div span</span>
</div>

<div>
    <ul>
        <li>
            <h2><span>div ul li h2 span</span></h2>
        </li>
    </ul>
</div>

<div>
    <h2><span>div h2 span</span></h2>
</div>
div:has(>h2>span) {
    margin-left: 24px;
    border: 1px solid #000;
}

这里,要求准确选择 div 下直接子元素是 h2,且 h2 下直接子元素有 span 的 div 元素。注意,选择的最上层使用 :has() 的父元素 div。结果如下:

这里体现的是嵌套结构,精确寻找对应的父元素

:has() 父选择器 -- 同级结构的兄元素选择

还有一种情况,在之前也比较难处理,同级结构的兄元素选择。

看这个 DEMO:

<div class="has-test">div + p</div>
<p>p</p>

<div class="has-test">div + h1</div>
<h1>h1</h1>

<div class="has-test">div + h2</div>
<h2>h2</h2>

<div class="has-test">div + ul</div>
<ul>ul</ul>

我们想找到兄弟层级关系中,后面接了 <h2> 元素的 .has-test 元素,可以这样写:

.has-test:has(+ h2) {
    margin-left: 24px;
    border: 1px solid #000;
}

效果如下:

这里体现的是兄弟结构,精确寻找对应的前置兄元素

这样,一直以来,CSS 没有实现的父选择器,借由 :has() 开始,也能够做到了。这个选择器,能够极大程度的提升开发体验,解决之前需要比较多 JavaScript 代码才能够完成的事。

上述 DEMO 汇总,你可以戳这里 CodePen Demo -- :has Demo

:has() 兼容性,给时间一点时间

比较可惜的是,:has() 在最近的 Selectors Level 4 规范中被确定,目前的兼容性还比较惨淡,截止至 2022-05-04,Safari 和 最新版的 Chrome(V101,可通过开启 Experimental Web Platform features 体验)

Chrome 下开启该特性需要,1. 浏览器 URL 框输入 chrome://flags,2. 开启 #enable-experimental-web-platform-features

耐心等待,给给时间一点时间,这么好的选择器马上就能大规模应用了。

最后

本文到此结束,希望对你有帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。