你可以用伪元素做的有趣事情

发布于 大漠

CSS 中的伪元素能做什么,其实在《伪元素能帮助我们做些什么》一文中就和大家探讨过。我们知道伪元素可以帮助我们 清除浮动制作Icon图标分割线CSS TooltipsCSS计数器 等事情。其实除了这些常见的事情之外,CSS 伪元素还可以帮助我们做一些非常有趣的事情,这些有趣的事情也称得上是 CSS 方面的小技巧吧。接下来,和大家聊聊这方面的话题。

可以用伪元素做些有趣的事情

正如《伪元素能帮助我们做些什么》一文所介绍的那样,CSS的伪元素 ::before::after 可以帮助我们做很多事情,并且在做这些事情的时候又可以减少 HTML 的结构。今天不想和大家深度聊伪元素如何使用,只是想和大家聊一些有趣而又适用的事情,或者说是小技巧吧。这些小技巧的思路都是来源于 Twitter 上的一些信息(我平日有逛Twitter)的习惯:

比如上图中所述的,使用伪元素构建渐变的蒙层、渐变阴影和渐变边框等。

当然,你可能知道使用其他的 CSS 技术来完成上面 Twitter 提到的效果,但接下来我们来看 CSS 伪元素是如何实现这些效果的。

CSS 的伪元素

前段时间在聊 CSS 计数器的奇妙世界(Part1Part2Part3) 的时候就花了很长的篇幅介绍了 CSS Generated Content Module Level 3

元素的::before::aftercontent 可以生成伪元素内容!

简单的说,元素的 ::before::after 相当于一个标签元素:

<div>content</div>

div::before,
div::after {
    content: ''
}

相当于:

<div>
    <div class="before"></div>
    content
    <div class="after"></div>
</div>    

这样一来,一个 HTML 元素就具备多个盒模型(多出两个盒子),如下图所示:

有了新增的伪元素,除了让 HTML 结构变得更简洁之外,还可以借助伪元素帮我们做更多有趣的事情。

卡片蒙层

在 Web 设计中,我们发现自己越来越多地把文字放在图片上面。很多时候,这样做对于用户体验来说是存有一定风险的。图片有动态的颜色和亮度,而文字在大多数情况下是一种固定颜色。这对于可读性和可访问性来说往往是一场恶梦。

这意味着我们要在图像和文字之间引入一个覆盖层(蒙层)。有时,这将使用背景图像变暗,足以保证可读性。针对这种场景,一般来说会有三个分层:

  • 图像
  • 渐变的蒙层(覆盖图片的层)
  • 内容层

假设我们要实现类似下面这样的一个横幅的效果:

为了节省不必要的 HTML 元素,上图效果中的渐变蒙层采用伪元素::before::after 来实现。这样一来,实现上面示例的 HTML 结构大致像下在这样:

<div class="hero">
    <div class="hero__content">
    </div>
    <img class="hero__figure" src="hero.png">
</div>

有不同的方法来实现这一点。如果图片纯粹是为了装饰目的,我们可以在.hero中使用background-image来替代<img>,否则像上面示例代码一样,使用 <img>元素。

添加 CSS 代码:

.hero {
    position: relative;
    min-height: 500px;
}

.hero__figure {
    position: absolute;
    inset: 0;
    object-fit: cover;
    object-position: center;
    width: 100%;
    height: 100%;
}

/* 伪元素制作蒙层 */
.hero::after {
    content: "";
    position: absolute;
    inset: 0;
    background-color: rgb(0 0 0 / 0.5);
}

.hero__content {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 1;
    transform: translate(-50%, -50%);
    text-align: center;
}

还可以把::after上的透明颜色换成带有透明度的渐变:

.hero {
    overflow: hidden;
}

.hero::after,
.hero::before {
    content: "";
    position: absolute;
    inset: 0 0 0 50%;
    background-image: linear-gradient(120deg, #eaee44, #33d0ff);
    background-color: rgb(0 0 0 / 0.5);
    opacity: 0.5;
    transform: skew(15deg) translateX(-50%);
    width: calc(100vw + 50%);
}

.hero::before {
    transform: skew(-15deg) translateX(-50%);
}

再加点animation的料:

.hero::after,
.hero::before {
    content: "";
    position: absolute;
    inset: 0 0 0 50%;
    background-image: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
    background-size: 400% 400%; /* 必不可少 */
    background-color: rgb(0 0 0 / 0.5);
    opacity: 0.5;
    transform: skew(15deg) translateX(-50%);
    width: calc(100vw + 50%);
    animation: gradient 5s ease infinite;
}

@keyframes gradient {
    0% {
        background-position: 0% 50%;
    }
    50% {
        background-position: 100% 50%;
    }
    100% {
        background-position: 0% 50%;
    }
}

你可以看到示例中渐变的蒙层会动起来,效果如下:

上面我们采用的是绝对定位实现的布局效果,其实我们可以用更现代的方法来实现层叠布局效果,即 CSS 网格布局

如果使用网格布局来构建上面示例横幅效果的话,需要在.hero中使用 display: grid 创建一个网格容器,然后其每个层都使用 grid-area: 1 / -1 将网格容器中的网格项目放置在同一个网格区域:

.hero {
    display: grid;
    height: 500px;
}

.hero__content {
    z-index: 1; /* ① */
    grid-area: 1 / -1;
    display: flex;
    flex-direction: column;
    margin: auto; /* ② */
    text-align: center;
}

.hero__figure {
    grid-area: 1 / -1;
    object-fit: cover; /* ③ */
    width: 100%;
    height: 100%;
    min-height: 0; /* ④ */
}

/* 伪元素制作蒙层 */
.hero::after,
.hero::before {
    content: "";
    background-image: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
    background-size: 400% 400%;
    background-color: rgb(0 0 0 / 0.5);
    opacity: 0.5;
    transform: skew(15deg);
    width: calc(100vw + 50%);
    animation: gradient 5s ease infinite;
    grid-area: 1 / -1;
}

示例中网格布局关键代码:

.hero {
    display: grid;
    height: 500px;
}

.hero__content {
    z-index: 1; /* ① */
    grid-area: 1 / -1;
    display: flex;
    margin: auto; /* ② */
}

.hero__figure {
    grid-area: 1 / -1;
    object-fit: cover; /* ③ */
    min-height: 0; /* ④ */
}

/* 伪元素制作蒙层 */
.hero::after,
.hero::before {
    grid-area: 1 / -1;
}

使用网格审查器,可以网格系统如下:

不幸运的是,我们需要在 .hero 网格容器上设置一个固定的高度,这样 .hero__figure 才能真正发挥作用。(一个高度为 100% 的子项目需要它的父元素有一个明确的固定高度,而不是最小高度)。

代码中标记着重介绍一下,因为他对于我们很重要

  • ①:可以在网格项目或Flex项目上使用z-index来控制项目在z轴的顺序,而不需要显式设置position:relative(或非staticposition)。详细请阅读《CSS定位和层叠控制》一文。
  • ②:因为.hero__content是一个网格项目,使用margin: auto 会使用它父容器中水平垂直居中
  • ③:在处理图片时,不要忘了给其显式设置 object-fit: cover
  • ④:在.hero__figure上使用min-height: 0,只是为了防止使用大型图片。这样做是迫使 CSS 网格的 height: 100%,并避免使用图像大于.hero容器高度

刚才提到过,如果图片是装饰图的话,可以直接在.hero中使用background-image。那么我们的 HTML 结构可以是:

<!-- HTML -->
<div class="hero">
    <div class="hero__content">
        <h2>Best Brownies in Town</h2>
        <p>High quality ingredients and best in-class chef</p>
        <a href="#">Order now</a>
    </div>
</div>

对应的 CSS 代码:

.hero {
    display: grid;
    height: 500px;
    background: url("https://picsum.photos/1920/500?random=1") no-repeat;
    background-size: cover;
    overflow: hidden;
    position: relative;
    isolation: isolate;
}

.hero__content {
    grid-area: 1 / -1;
    display: flex;
    flex-direction: column;
    margin: auto;
    text-align: center;
}

/* 伪元素制作蒙层 */
.hero::after,
.hero::before {
    content: "";
    background-image: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
    background-size: 400% 400%;
    background-color: rgb(0 0 0 / 0.5);
    opacity: 0.5;
    transform: skew(15deg) translateX(-25%);
    width: calc(100vw + 50%);
    animation: gradient 5s ease infinite;
    grid-area: 1/-1;
    z-index: -1;
}

.hero::before {
    transform: skew(-15deg) translateX(-25%);
}

@keyframes gradient {
    0% {
        background-position: 0% 50%;
    }
    50% {
        background-position: 100% 50%;
    }
    100% {
        background-position: 0% 50%;
    }
}

注意,在这个示例中,我们使用了个大家比较少见的 CSS 属性,即 CSS 的 isolation 属性。isolation 属性定义了该元素是否必须创建一个新的层叠上下文。其主要作用是当和 background-blend-mode 属性一起使用时,可以只混合一个指定元素栈的背景:它允许一组元素从它们后面的背景中独立出来,只混合这组元素的背景

有关于 isolation 属性更详细的介绍可以阅读 @张鑫旭老师的 《理解CSS3 isolation: isolate 的表现和作用》一文。

上面示例中的横幅的蒙层效果还可以运用于卡片的效果中:

<!-- HTML -->
<article class="card">
    <div class="card__thumb">
        <img src="https://picsum.photos/500?random=25" alt="">
    </div>
    <div class="card__content">
        <h2><a href="#">Cinnamon Rolls</a></h2>
        <p>Light, tender, and easy to make</p>
    </div>
    <span class="card__tag">Must Try</span>
</article>

对应的 CSS 代码:

.card {
    position: relative;
    display: grid;
    overflow: hidden;
    isolation: isolate;
}

.card__thumb {
    object-fit: cover;
    max-width: 100%;
    aspect-ratio: 4/3;
    width: 100%;
    z-index: -3;
}

.card > * {
    grid-area: 1 / -1;
}

.card::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background: linear-gradient(0deg, rgb(0 0 0 / 0.7), rgb(0 0 0 / 0));
    z-index: -1;
}

你甚至还可以在 ::before::after 使用 CSS 的滤镜,让蒙层更具艺术效果:

.card {
    --clr-orange: 7 100% 63%;
    --clr-blue: 239 100% 50%;
}

.card::before {
    z-index: -1;
    background-color: hsl(var(--clr-orange));
    mix-blend-mode: multiply;
}

.card::after {
    z-index: -2;
    background-color: hsl(var(--clr-blue));
    mix-blend-mode: screen;
}

渐变边框

在《图解CSS: CSS渐变》一文中,有介绍过使用 CSS 渐变来构建渐变边框的技术方案。比如在 border-image运用渐变和多背景结合等。

简单回忆一下这些制作渐变边框的技术。先来看 border-image和CSS渐变的组合:

<div style="--gradient: linear-gradient(red, gold)"></div>

div {
    border: 10px solid;
    border-image: var(--gradient) 1;
}

效果如下:

border-image上使用渐变构建的渐边边框不能和border-radius结合使用,即使使用了,border-radius也会被忽略。但是我们可以结合clip-path来实现:

div {
    border: 10px solid;
    border-image: var(--gradient) 1;
    clip-path: inset(0 round 10px);
}

效果如下:

如果你对CSS的clip-path感兴趣的话,可以移步阅读《探索CSS Masking模块:Clipping》一文。

实现渐变边框还可以结合background-clipbackground-origin和多个渐变来实现。这种方式也常称为是嵌套渐变。例如:

div {
    border: 10px solid transparent;
    background-image: linear-gradient(#222, #222), var(--gradient);
    background-origin: border-box;
    background-clip: padding-box, border-box; 
    border-radius: 10px;
}

效果如下:

除了上面的技术方案,还可以使用伪元素::before::after

.card {
    --borderWidth: 4px;
    position: relative;
}

.card::after {
    content: "";
    position: absolute;
    inset: calc(-1 * var(--borderWidth));
    background: linear-gradient(
        60deg,
        #f79533,
        #f37055,
        #ef4e7b,
        #a166ab,
        #5073b8,
        #1098ad,
        #07b39b,
        #6fba82
    );
    border-radius: calc(2 * var(--borderWidth));
    z-index: -1;
}

示例中我们使用了 inset,相当于分别设置了toprightbottomleft的值,并且使用 clac() 函数和 CSS 自定义属性的结合,来控制位移和边框大小相匹配:

.card {
    --borderWidth: 4px;
}

.card::after {
    inset: calc(-1 * var(--borderWidth));
}

相当于:

.card {
    --borderWidth: 4px;
}

.card::after {
    top: calc(-1 * var(--borderWidth));
    right: calc(-1 * var(--borderWidth));
    bottom: calc(-1 * var(--borderWidth));
    left: calc(-1 * var(--borderWidth));
}

我们也可以把inset的值设置为0,然后通过margin的负值来将元素向容器四个边缘往外拉:

.card {
    --borderWidth: 4px;
    position: relative;
}

.card::after {
    content: "";
    position: absolute;
    inset: 0;
    margin: calc(-1 * var(--borderWidth));
    background: linear-gradient(
        60deg,
        #f79533,
        #f37055,
        #ef4e7b,
        #a166ab,
        #5073b8,
        #1098ad,
        #07b39b,
        #6fba82
    );
    border-radius: calc(2 * var(--borderWidth));
    z-index: -1;
}

同样的,我们也可以使用 CSS Grid 布局,这样伪元素可以不使用position定位,但内容需要用额外的标签来包裹:

<div class="card">
    <div class="card__content">Gradient Border</div>
</div>    

.card {
    --borderWidth: 4px;
    display: grid;
    position: relative;
}

.card__content {
    grid-area: 1 / -1;
    background: #fff;
    width: 40vw;
    aspect-ratio: 4 / 3;
    border-radius: 10px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: clamp(20px, 3vw + 2rem, 30px);
}

.card::after {
    content: "";
    grid-area: 1 / -1;
    margin: calc(-1 * var(--borderWidth));
    background: linear-gradient(
        60deg,
        #f79533,
        #f37055,
        #ef4e7b,
        #a166ab,
        #5073b8,
        #1098ad,
        #07b39b,
        #6fba82
    );
    border-radius: calc(2 * var(--borderWidth));
    z-index: -1;
}

另外再来看另一种技术方案,就是mask、渐变和伪元素的结合:

.card {
    --borderWidth: 5px;
    position: relative;
    background: #fff;
    width: 40vw;
    aspect-ratio: 4 / 3;
    border-radius: 10px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: clamp(20px, 3vw + 2rem, 30px);
}

.card::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: calc(2 * var(--borderWidth));
    padding: var(--borderWidth);
    background: linear-gradient(
        60deg,
        #f79533,
        #f37055,
        #ef4e7b,
        #a166ab,
        #5073b8,
        #1098ad,
        #07b39b,
        #6fba82
    );
    mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
    mask-composite: exclude;
}

示例中用到了 CSS mask 中的合成特性mask-composite

遮罩合成指的是我们可以使用不同的操作将多个不同的遮罩层合并成一个独立的遮罩层。有关于这方面更详细的介绍可以阅读:

如果你使用background-size调整渐变的尺寸,配合@keyframes还以制作带有动效的渐变边框:

.card::after {
    content: "";
    position: absolute;
    inset: calc(-1 * var(--borderWidth));
    background: linear-gradient(
        60deg,
        #f79533,
        #f37055,
        #ef4e7b,
        #a166ab,
        #5073b8,
        #1098ad,
        #07b39b,
        #6fba82
    );
    border-radius: calc(2 * var(--borderWidth));
    z-index: -1;
    animation: animatedgradient 3s ease alternate infinite;
    background-size: 400% 400%;
}

@keyframes animatedgradient {
    0% {
        background-position: 0% 50%;
    }
    50% {
        background-position: 100% 50%;
    }
    100% {
        background-position: 0% 50%;
    }
}

随着Web页面设计效果越来越丰富,渐变边框在实际使用也越来越频繁,比如下图(CSS-TRICKS网站卡片):

甚至更复杂的效果:

具代码如下:

渐变阴影

众所周知,在 CSS 中时常使用 box-shadowfilterdrop-shadow() 来给一个盒子添加阴影。这两者最大的区别就是 filterdrop-shadow()可以给不规则的图形(或元素)添加阴影:

事实上,阴影是光线照射到一个物体上投下的阴影,此时,这种光线照射下产生的阴影会呈现出无数独特的特征。如果你试图使用 box-shadowdrop-shadow() 来捕获真正的阴影是微妙之处难度就大了,甚至是有点不可能。因为 CSS 中的 box-shaodwdrop-shadow() 本质上是产生一个模糊的物体轮廓,你可以改变阴影的偏移量,模糊半径,阴影扩散和阴影颜色等,但也仅此而已。我们无法接近表达现实生活中阴影的复杂性和细微差别。

但通过一个简单的 CSS 技术,我们可以扩大我们的选择范围。如果我们使用分层的 box-shadow,就可以对阴影的呈现方式进行更精细的控制。比如下面两个示例,左侧是单一(未分层)盒子阴影,右侧是分层盒子阴影。分层盒子阴影通过创建多个盒子阴影(用逗号分隔的每个阴影),并增加每个阴影的偏移量和模糊度来实现这种效果:

.box {
    box-shadow: 0 3px 3px rgba(0,0,0,0.2);
}

.shadow {
    box-shadow: 0 1px 1px rgba(0,0,0,0.12), 
                0 2px 2px rgba(0,0,0,0.12), 
                0 4px 4px rgba(0,0,0,0.12), 
                0 8px 8px rgba(0,0,0,0.12),
                0 16px 16px rgba(0,0,0,0.12);
}

如果你觉得这种平滑的盒子阴影不好控制,可以使用 @brumm 提供的工具:

通过分层构建的盒子阴影效果更细腻,更真实,但它对于性能而言是致命的:box-shadow很慢。更糟糕的是,通过动画化的box-shadow来使一个元素感觉它在向前或向后移动(这是一种常见的设计模式)。阴影在每一帧改变时都会导致重绘,所以阴影的过渡(transitionanimation)速度非常慢。为了让盒子阴影不给 Web 性能带来阴影,时常通过增一个伪元素来构建阴影,通过改变伪元素的opacity 值(从0 过渡到1)来实现阴影的过渡动效:

如果你使用开发工具尝试了其中之一,您应该会看到类似这样的东西 (绿色条表示已经绘制,其越少越好):

当你悬停在左边的卡片(在box-shadow上应用动画)与悬浮在右边的卡片(对其伪元素的opacity应用动画)进行相比时,你会很明显的发现有更多的重新绘制。

利用同样的原理,在伪元素::before::after上单独绘制一个和前景相似的渐变(background:inherit即可),再使用filterblur()把伪元素变得模糊,让其看上去就是一个阴影:

:root {
    --purple: #7f00ff;
    --pink: #e100ff;
}

button {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    position: relative;
    outline: none;
    border: none;
    font-family: "Exo", Arial, sans-serif;
    -webkit-appearance: none;
    -webkit-tap-highlight-color: transparent;
    cursor: pointer;
    user-select: none;
    padding: 1em 2em;
    border-radius: 50px;
    color: #fff;
    min-width: 20vw;
    font-size: 2rem;

    background: linear-gradient(to right, var(--purple), var(--pink));
}

button::after {
    content: "";
    position: absolute;
    z-index: -1;
    inset: 0;
    opacity: 0.8;
    border-radius: 10rem;
    background: inherit; /* 很重要 */
    filter: blur(30px);   /* 很重要 */

    transition: all 0.2s;
}

button:hover::after {
    filter: blur(32px);
    bottom: -5px;
}

button:hover:active::after {
    filter: blur(10px);
    bottom: -6px;
}

把渐变配置上CSS自定义属性,就可以构建一个简单的渐变阴影小工具了:

我们还可以把渐变边框和渐变阴影结合在一起,实现类似下面这个示例的效果:

伪逗号

有的时候,你可能想在一个段落或一个声明中显示你的列表项。在适当的地方必须有逗号(,)和 and。这就是 伪逗号 诞生的原因。 @ShadowShahriar 在 Codepen 上创建了一个示例,使用伪元素在内联显示的列表项之间放置逗号,其结果是一个具有正确标点符号的自然的完整句子。

这个也称得上是 CSS 伪元素的一个有趣之处吧,也是一个 CSS 小技巧。 点击示例的复选框,效果如下所示:

其决窍很简单。首先,让无序列表成为没有标记或间距的内联元素:

ul {
    padding: 0;
    margin: 0;
    display: inline;
    list-style-type: none;
}

接下来,我们以内联方式显示列表项,使它们像句子中的文字一样自然流动:

li {
    display: inline;
}

然后,我们通过选择器列表项的::after元素,并将其 content 属性设置为逗号(,)值,在列表项之间添加逗号:

ul {
    --separator: ",";
}

li::after{
    content: var(--separator);
}

同时使用:nth-last-of-type()选择倒数第二的列表项,并将其::aftercontent设置为, and

li:nth-last-of-type(2)::after{
    content: ", and ";  
}

@ShadowShahriar考虑了一个只有两个项目的边缘情况。我们所需要的是在这两个项目之间显示一个 and ,所以:

li:first-of-type:nth-last-of-type(2)::after {
    content: " and ";
}

最后,给最后一个列表项的伪元素::aftercontent 设置为一个句号(.):

li:last-of-type::after {
    content: ".";
}

如果把 CSS 自定义属性结合进来,可以这样做:

ul{
    --separator: ",";
    --connector: "and";

    padding: 0;
    margin: 0;
    display: inline;
    list-style-type: none;
}

li{
    display: inline;
}

li::after{
    content: var(--separator);
    color: #ef5016;
    transition: color ease 200ms;
    font-weight: 700;
}

li:nth-last-of-type(2)::after{
    content: var(--separator) " " var(--connector) " ";
    color: #0058ff;
}

li:first-of-type:nth-last-of-type(2)::after{
    content: " " var(--connector);
    color: #178717;
}

li:last-of-type::after{
    content: ".";
    color: #0000ff;
}

你可以根据语言或设计风格给--separator--connector 设置你想要的值。

它最大的特点就是巧妙的使用了 CSS 伪元素的技巧,而且还因为它的简单性。它以一种支持 HTML 语义的方式使用了久经考验的 CSS 原则(没有额外的类、元素,甚至没有任何 JavaScipt脚本)。

伪元素的动效

正如前面示例所示,我们可以在伪元素上使用 animationtransition 来创建一些有趣的效果。比如 @Marco Barria 在 《Examples of Pseudo-Elements Animations and Transitions》写的水滴掉落的动效:

<!-- HTML -->
<div class="drop"></div>

.drop {
    background: rgba(255, 255, 245, 1);
    border: 4px solid rgba(255, 245, 235, 1);
    border-radius: 100%;
    box-shadow: inset -0.1em 0 2em 0.5em rgba(255, 255, 255, 0.5),
        inset -0.1em 0 0.5em 0 rgba(0, 0, 0, 0.8);
    position: relative;
    margin: 0 auto;
    width: 15em;
    height: 15em;
    overflow: hidden;
    transform-style: preserve-3d;
}

.drop::before,
.drop::after {
    content: "";
    display: block;
    position: absolute;
}

.drop::before {
    background: rgba(167, 217, 234, 1);
    border-radius: 100%;
    box-shadow: 0 0 0 0.1em rgba(167, 217, 234, 0.8),
        0 0 0 0.15em rgba(167, 217, 234, 0.8), 0 0 0 0.2em rgba(167, 227, 234, 0.8),
        0 0 0 0.25em rgba(167, 227, 234, 0.8), 0 0 0 0.3em rgba(167, 227, 234, 0.8),
        0 0 0 0.35em rgba(167, 227, 234, 0.8), 0 0 0 0.4em rgba(167, 227, 234, 0.8),
        0 0 0 0.45em rgba(167, 227, 234, 0.8), 0 0 0 0.5em rgba(167, 227, 234, 0.8);
    top: 0%;
    left: 50%;
    width: 0.2em;
    height: 0.2em;

    animation: fall 3.5s cubic-bezier(0.5, 0, 1, 0.5) infinite;
}

.drop::after {
    background: rgb(52, 152, 219);
    background: linear-gradient(
        rgba(52, 255, 255, 1) 0%,
        rgba(52, 152, 219, 1) 10%,
        rgba(152, 252, 219, 1) 100%
    );
    border-radius: 100% 0 50% 0;
    left: 0;
    bottom: 0;
    width: inherit;
    height: 3em;
    opacity: 0.7;
    animation: surface 3s linear infinite;
}

@keyframes fall {
    5%,
    15% {
        box-shadow: 0 -1.4em 0 0.1em rgba(167, 217, 234, 1),
        0 -0.8em 0 0.15em rgba(167, 217, 234, 1),
        0 -0.3em 0 0.2em rgba(167, 217, 234, 1),
        0 -0.1em 0 0.25em rgba(167, 217, 234, 1),
        0 0 0 0.3em rgba(167, 217, 234, 1),
        0 0.2em 0 0.35em rgba(167, 217, 234, 1),
        0 0.4em 0 0.4em rgba(167, 217, 234, 1),
        0 0.6em 0 0.45em rgba(167, 217, 234, 1),
        0 0.8em 0 0.5em rgba(167, 217, 234, 1);
    }
    16% {
        top: 80%;
    }
    18% {
        top: 80%;
        box-shadow: 1em -8em 0 0.2em rgba(177, 227, 234, 1),
        -2.2em -3.8em 0 0.1em rgba(177, 227, 234, 1),
        3em -2.85em 0 0.3em rgba(177, 227, 234, 1),
        -3.5em -4em 0 0.2em rgba(177, 227, 234, 1),
        0 0 0 0.3em rgba(177, 227, 234, 1),
        2em -2em 0 0.2em rgba(177, 227, 234, 1),
        -0.3em -3em 0 0.2em rgba(177, 227, 234, 1),
        0.5em -5em 0 0.35em rgba(177, 227, 234, 1),
        -3em -1em 0 0.3em rgba(177, 227, 234, 1);
    }
    30% {
        top: 90%;
        box-shadow: 1.5em 0 0 0.2em rgba(252, 252, 255, 0.1),
        -2em 0 0 0.15em rgba(252, 252, 255, 0.1),
        3em 0 0 0.2em rgba(252, 252, 255, 0.1),
        -2em 0 0 0.25em rgba(252, 252, 255, 0.1),
        0 0 0 0.2em rgba(252, 252, 255, 0.1),
        2.35em 0 0 0.3em rgba(252, 252, 255, 0.1),
        -0.5em 0 0 0.2em rgba(252, 252, 255, 0.1),
        1em 0 0 0.3em rgba(252, 252, 255, 0.1),
        -4em 0 0 0.4em rgba(252, 252, 255, 0.1);
    }
    40%,
    100% {
        top: 95%;
        background: rgba(255, 255, 255, 1);
        box-shadow: 1.8em 0.5em 0 0.2em rgba(255, 255, 255, 0),
        -3em 0.5em 0 0.1em rgba(255, 255, 255, 0),
        4em 0.5em 0 0.1em rgba(255, 255, 255, 0),
        -3.5em 0.5em 0 0.1em rgba(255, 255, 255, 0),
        0 0 0 0.3em rgba(255, 255, 215, 0),
        2.45em 0.5em 0 0.1em rgba(255, 255, 255, 0),
        -0.8em 0.5em 0 0.2em rgba(255, 255, 255, 0),
        1.5em 0.5em 0 0.3em rgba(255, 255, 255, 0),
        -4.5em 0.5em 0 0.2em rgba(255, 255, 255, 0);
    }
}

@keyframes surface {
    50% {
        border-radius: 0 75% 0 75%;
        opacity: 0.5;
        height: 3.5em;
    }
}

我们在《伪元素能帮助我们做些什么》和《CSS的视觉效果》中介绍过,CSS 的伪元素还可以帮助我们绘制一些图形,比如,我们绘制一个 VisBug的 Logo,然后结合 CSS 自定义属性,让这只小虫跟随鼠标飞起来:

<!-- HTML -->
<visbug-logo>
    <div class="head"></div>
    <div class="blend-containment">
        <div class="body"></div>
    </div>
</visbug-logo>

/* CSS */
:root {
    --mouse-x: 50vw;
    --mouse-y: 50vh;
}

visbug-logo {
    inline-size: 50vmin;
    block-size: 50vmin;
    display: grid;
    grid: [stack] 1fr / [stack] 1fr;
    transform: rotateY(0.5turn);
    position: absolute;
    left: var(--mouse-x);
    top: var(--mouse-y);
    cursor: pointer;
}

visbug-logo > * {
    grid-area: stack;
}

.head {
    --size: 20vmin;
    --size-center-offset: calc(var(--size) / 2 * -1);

    inline-size: var(--size);
    block-size: var(--size);
    margin-top: var(--size-center-offset);
    margin-left: var(--size-center-offset);
    border-radius: 100vmax;
    background: black;
}

.body {
    background: yellow;
    border-radius: 25% 100% 0% 100% / 0% 100% 0% 100%;
}

@property --rotation {
    syntax: "<deg>";
    inherits: false;
    initial-value: 12deg;
}

.blend-containment::before,
.blend-containment::after {
    content: "";
    display: block;
    --rotation: 12deg;

    border-radius: 0% 100% 0% 100% / 0% 100% 0% 100%;
    transform-origin: top left;
    mix-blend-mode: darken;
    transition: transform 0.2s ease;
    grid-area: stack;
}

.blend-containment::before {
    background: magenta;
    transform: rotateZ(var(--rotation));
}

.blend-containment::after {
    background: cyan;
    transform: rotateZ(calc(-1 * var(--rotation)));
}

.blend-containment {
    display: grid;
    grid: [stack] 1fr / [stack] 1fr;
    isolation: isolate;
}

.blend-containment:hover::before,
.blend-containment:hover::after {
    --rotation: 25deg;
    animation: rotate 1s linear infinite;
}

.blend-containment > .body {
    grid-area: stack;
}

@keyframes rotate {
    from {
        --rotation: 12deg;
    }
    to {
        --rotation: 25deg;
    }
}

// JavaScript
let root = document.documentElement;

root.addEventListener("mousemove", (e) => {
    root.style.setProperty("--mouse-x", e.clientX + "px");
    root.style.setProperty("--mouse-y", e.clientY + "px");
});

在屏幕上移动鼠标,你能看到下面这样的效果:

除此之外,还可以构建很有意思的,有创意的效果。比如 @littlesnippets 在 Codepen 上是使用这种方式构建很多鼠标悬浮在图片上的动效:

我们来看其中一个Demo的效果:

小结

我们曾在《伪元素能帮助我们做些什么》介绍了 CSS 伪元素常使用的地方,比如清除浮动、绘制图标、制作分割线、提示框、自动计数器、扩展可点击区域等。在《CSS的视觉效果》介绍了如何借助 CSS 伪元素构建丰富的视觉效果。今天这篇文章同样是围绕着CSS的伪元素,主要介绍几个大家平时不怎么用,但又非常有趣的事情,比如卡片蒙层、渐变边框、渐变阴影、伪逗号等。希望这几个小技巧能帮助大家解决平时工作中的需求。