我最近偶然发现了一种名为 玻璃质感(glassmorphism) 的酷炫效果,是在 一个 Dribbble 作品 中看到的。我的第一反应是,如果我只是使用一些表情符号作为图标,而不浪费时间去制作 SVG,我可以在几分钟内快速重现它。

我关于“几分钟”的判断错得不能再错了——最终我花了几天时间才挠到这个痒痒!
事实证明,虽然有一些资源介绍如何用 CSS 实现这种效果,但它们都假设了一种非常简单的情况,即覆盖层是矩形,或者最多是带 border-radius
的矩形。然而,对于像图标这样的不规则形状(无论是表情符号还是正常的 SVG)实现玻璃质感效果,比我预期的要复杂得多,所以我认为分享一下这个过程、我遇到的陷阱和学到的东西是值得的。还有我仍然不理解的东西。
为什么选择表情符号?
简短回答:因为 SVG 太耗时。详细回答:因为我缺乏在图像编辑器中直接绘制图标的艺术感,但我对语法足够熟悉,以至于我经常可以将在线找到的现成 SVG 压缩到其原始大小的 10% 以下。所以,我不能原封不动地使用从互联网上找到的 SVG——我必须重写代码使其超级简洁和紧凑。而这需要时间。很多时间,因为这是细节工作。
如果我只想快速编写一个带图标的菜单概念,我会求助于使用表情符号,并对其应用滤镜使其与主题相匹配,仅此而已!这就是我在这个 液体标签栏交互 演示中所做的——那些图标都是表情符号!平滑的谷效应利用了 蒙版合成 技术。

好了,这将是我们的起点:使用表情符号作为图标。
最初的想法
我的第一想法是将导航链接的两个伪元素(带有表情符号内容)叠加,稍微偏移一下,并使用 transform
旋转下面的那个,使它们仅部分重叠。然后,我将使用小于 1
的 opacity
值使上面的那个半透明,在其上设置 backdrop-filter: blur()
,这应该就足够了。
现在,在阅读了引言后,您可能已经猜到事情并没有按计划进行,但让我们看看它在代码中的样子以及存在哪些问题。
我们使用以下 Pug 生成导航栏
- let data = {
- home: { ico: '🏠', hue: 200 },
- notes: { ico: '🗒️', hue: 260 },
- activity: { ico: '🔔', hue: 320 },
- discovery: { ico: '🧭', hue: 30 }
- };
- let e = Object.entries(data);
- let n = e.length;
nav
- for(let i = 0; i > n; i++)
a(href='#' data-ico=e[i][1].ico style=`--hue: ${e[i][1].hue}deg`) #{e[i][0]}
编译成下面的 HTML
<nav>
<a href='#' data-ico='🏠' style='--hue: 200deg'>home</a>
<a href='#' data-ico='🗒️' style='--hue: 260deg'>notes</a>
<a href='#' data-ico='🔔' style='--hue: 320deg'>activity</a>
<a href='#' data-ico='🧭' style='--hue: 30deg'>iscovery</a>
</nav>
我们从布局开始,将我们的元素设置为网格项。我们将导航栏放置在中间,为链接指定宽度,将每个链接的两个伪元素都放在顶部单元格(这会将链接文本内容推到底部单元格),并将链接文本和伪元素居中对齐。
body, nav, a { display: grid; }
body {
margin: 0;
height: 100vh;
}
nav {
grid-auto-flow: column;
place-self: center;
padding: .75em 0 .375em;
}
a {
width: 5em;
text-align: center;
&::before, &::after {
grid-area: 1/ 1;
content: attr(data-ico);
}
}

请注意,表情符号的外观会因您用来查看演示的浏览器而异。
我们选择一个易读的 font
,增加其大小,使图标更大,设置背景,并为每个链接设置更好看的 color
(基于每个链接 style
属性中的 --hue
自定义属性)
body {
/* same as before */
background: #333;
}
nav {
/* same as before */
background: #fff;
font: clamp(.625em, 5vw, 1.25em)/ 1.25 ubuntu, sans-serif;
}
a {
/* same as before */
color: hsl(var(--hue), 100%, 50%);
text-decoration: none;
&::before, &::after {
/* same as before */
font-size: 2.5em;
}
}

从这里开始,事情变得有趣起来,因为我们开始区分使用链接伪元素创建的两个表情符号层。我们稍微移动并旋转 ::before
伪元素,使用 sepia(1)
滤镜将其变为单色,使其达到正确的色调,并提高其 contrast()
——一个古老但经典的技术,来自 Lea Verou。我们还在 ::after
伪元素上应用 filter: grayscale(1)
并使其半透明,因为否则我们将无法透过它看到另一个伪元素。
a {
/* same as before */
&::before {
transform:
translate(.375em, -.25em)
rotate(22.5deg);
filter:
sepia(1)
hue-rotate(calc(var(--hue) - 50deg))
saturate(3);
}
&::after {
opacity: .5;
filter: grayscale(1);
}
}

遇到障碍
到目前为止,一切都很好……所以呢?下一步,当我想到编写这个代码时,我愚蠢地认为这将是最后一步,它涉及到在顶部(::after
)层上设置 backdrop-filter: blur(5px)
。
请注意,Firefox 仍然需要将 about:config
中的 gfx.webrender.all
和 layout.css.backdrop-filter.enabled
标志设置为 true
,才能使 backdrop-filter
属性生效。

backdrop-filter
生效的标志。遗憾的是,结果与我预期的完全不同。我们得到了一种覆盖整个顶部图标边界框的覆盖层,但底部图标并没有真正模糊。

backdrop-filter
后。然而,我敢肯定我之前玩过 backdrop-filter: blur()
,而且它确实有效,那么这里到底发生了什么?

找到问题的根源
好吧,当你完全不知道为什么某些东西不起作用时,你所能做的就是拿另一个有效的示例,开始对其进行调整以尝试获得你想要的结果……然后看看它在哪里崩溃!
因此,让我们看看我之前有效的演示的简化版本。HTML 只是一个 section
中的 article
。在 CSS 中,我们首先设置一些尺寸,然后在 section
上设置图像 background
,在 article
上设置半透明的背景。最后,我们在 article 上设置 backdrop-filter
属性。
section { background: url(cake.jpg) 50%/ cover; }
article {
margin: 25vmin;
height: 40vh;
background: hsla(0, 0%, 97%, .25);
backdrop-filter: blur(5px);
}

这有效,但我们不希望我们的两层嵌套在一起;我们希望它们成为兄弟元素。因此,让我们将两层都设为 article
兄弟元素,使它们部分重叠,看看我们的玻璃质感效果是否仍然有效。
<article class='base'></article>
<article class='grey'></article>
article { width: 66%; height: 40vh; }
.base { background: url(cake.jpg) 50%/ cover; }
.grey {
margin: -50% 0 0 33%;
background: hsla(0, 0%, 97%, .25);
backdrop-filter: blur(5px);
}

在 Chrome 中一切似乎都很好,在 Firefox 中也大部分如此。只是 Firefox 在边缘处理 blur()
的方式看起来很奇怪,不是我们想要的。而且,根据规范中的几张图片,我相信 Firefox 的结果也是不正确的?
我想,如果我们的两层位于纯色 background
上(在本例中为 white
),解决 Firefox 问题的办法是,为底部层(.base
)添加一个 box-shadow
,没有偏移量,没有模糊,并且扩展半径是我们应用于顶层(.grey
)的 backdrop-filter
的模糊半径的两倍。当然,这个修复 似乎在我们的特定情况下有效。
如果我们的两层位于具有非 fixed
图像 background
的元素上,事情会变得复杂得多(在这种情况下,我们可以使用分层背景 方法 来解决 Firefox 问题),但这里并非如此,因此我们不会深入讨论。
尽管如此,让我们继续下一步。我们不希望我们的两层是两个方形盒子,我们希望它们是表情符号,这意味着我们无法使用 hsla()
背景确保顶部层的半透明度——我们需要使用 opacity
。
.grey {
/* same as before */
opacity: .25;
background: hsl(0, 0%, 97%);
}

opacity
而不是 hsla()
背景使顶层半透明时的结果(实时演示)。看起来我们找到了问题!出于某种原因,使用 opacity
使顶层半透明会破坏 Chrome 和 Firefox 中的 backdrop-filter
效果。这是个错误吗?这是预期的行为吗?
错误还是正常现象?
MDN 在 backdrop-filter
页面 的第一段中这样说道
因为它应用于元素**后面**的所有内容,因此要查看效果,您必须使元素或其背景至少部分透明。
除非我不理解上面这句话,否则这似乎表明 opacity
不应该破坏效果,即使它在 Chrome 和 Firefox 中确实破坏了效果。
规范说了什么?好吧,规范 是一大堆文字,没有多少插图或交互式演示,而且用一种让人读起来像闻臭鼬腺体一样令人反感的语言写成。它包含这部分内容,我感觉可能与之相关,但我不确定我是否理解它想表达的意思——在顶部元素上设置的 `opacity`,我们也在其上设置了 `backdrop-filter`,也会应用到它下面的兄弟元素上?如果这是预期的结果,那么在实践中肯定不会发生。
除非元素 B 的一部分是半透明的,否则 `backdrop-filter` 的效果将不可见。另请注意,应用于元素 B 的任何 `opacity` 也将应用于经过滤的背景图像。
尝试随机操作
无论规范可能在说什么,事实仍然是:使用 `opacity` 属性使顶层半透明会破坏 Chrome 和 Firefox 中的玻璃质感效果。还有其他方法可以使表情符号半透明吗?好吧,我们可以尝试 `filter: opacity()`!
在这一点上,我可能应该报告一下这种替代方案 是否有效,但现实是……我不知道!我在这步上花了两天时间,期间无数次检查了测试——有时它在相同的浏览器中有效,有时无效,结果会根据一天中的时间而有所不同。我还在 Twitter 上询问,得到了不同的答案。就在那一刻,你忍不住会怀疑某个万圣节幽灵是否在困扰、惊吓和吓坏你的代码。永远!
看起来所有希望都破灭了,但让我们再尝试一件事:用文本替换矩形,顶部文本使用 `color: hsla()` 设置为半透明。我们可能无法获得我们想要的酷炫表情符号玻璃质感效果,但也许我们可以对纯文本获得这样的效果。
因此,我们将文本内容添加到我们的 `article` 元素中,放弃它们的显式大小,增加它们的 `font-size`,调整使我们部分重叠的 `margin`,最重要的是,用 `color` 声明替换最后一个工作版本中的 `background` 声明。出于可访问性原因,我们还在底部元素上设置了 `aria-hidden='true'`。
<article class='base' aria-hidden='true'>Lion 🧡</article>
<article class='grey'>Lion 🖤</article>
article { font: 900 21vw/ 1 cursive; }
.base { color: #ff7a18; }
.grey {
margin: -.75em 0 0 .5em;
color: hsla(0, 0%, 50%, .25);
backdrop-filter: blur(5px);
}

这里需要注意几点。
首先,**将 `color` 属性设置为具有子单位 alpha 的值也会使表情符号半透明,而不仅仅是纯文本**,这在 Chrome 和 Firefox 中都是如此!这是我以前从未知道的事情,我发现它绝对令人难以置信,因为其他通道根本不会影响表情符号。
其次,Chrome 和 Firefox 都模糊了橙色文本和表情符号在顶部半透明灰色层边界框下方的整个区域,而不是只模糊实际文本下方的内容。在 Firefox 中,由于那个笨拙的锐利边缘效果,情况看起来更糟。
即使框模糊不是我们想要的,但我还是忍不住认为它是有道理的,因为规范确实说了以下内容
[...]要创建一个“透明”元素以显示完整的过滤背景图像,可以使用“background-color: transparent;”。
因此,让我们做一个测试来检查当顶层是另一个非矩形形状(不是文本,而是通过 `background` 渐变、`clip-path` 或 `mask` 获得)时会发生什么!

在 Chrome 和 Firefox 中,当形状通过 `background: gradient()` 获得时,顶层整个框下方的区域都会被模糊,正如前面文本案例中提到的,这在规范中是有道理的。但是,Chrome 尊重 `clip-path` 和 `mask` 形状,而 Firefox 则不尊重。而且,在这种情况下,我真的不知道哪一个是正确的,尽管 Chrome 的结果对我来说更有意义。
转向 Chrome 解决方案
这个结果和我询问如何使模糊尊重文本边缘而不是其边界框边缘时获得的 Twitter 建议让我迈出了 Chrome 的下一步:在顶层(`.grey`)上应用一个剪裁到 `text` 的 `mask`。此解决方案在 Firefox 中不起作用,原因有两个:一、`text` 可悲地是一个非标准的`mask-clip` 值,仅在 WebKit 浏览器中有效;二、如上测试所示,无论如何,遮罩在 Firefox 中都不会将模糊区域限制在 `mask` 创建的形状内。
/* same as before */
.grey {
/* same as before */
-webkit-mask: linear-gradient(red, red) text; /* only works in WebKit browsers */
}

好吧,这实际上看起来像是我们想要的,所以我们可以说我们正在朝着正确的方向前进!但是,这里我们使用了橙色心形表情符号作为底层,黑色心形表情符号作为顶部半透明层。其他通用表情符号没有黑白版本,所以我的下一个想法是最初使两层相同,然后使顶层半透明并在其上使用 `filter: grayscale(1)`。
article {
color: hsla(25, 100%, 55%, var(--a, 1));
font: 900 21vw/ 1.25 cursive;
}
.grey {
--a: .25;
margin: -1em 0 0 .5em;
filter: grayscale(1);
backdrop-filter: blur(5px);
-webkit-mask: linear-gradient(red, red) text;
}

好吧,这确实对顶层产生了我们想要的效果。不幸的是,由于某种奇怪的原因,它似乎也影响了下方层的模糊区域。这一刻是短暂考虑将笔记本电脑扔出窗外的时候……然后想到再添加一层。
它将是这样的:我们有基础层,就像我们到目前为止所做的那样,稍微偏离上面另外两个层。中间层是一个“幽灵”(透明)层,应用了 `backdrop-filter`。最后,顶层是半透明的,并获得 `grayscale(1)` 过滤器。
body { display: grid; }
article {
grid-area: 1/ 1;
place-self: center;
padding: .25em;
color: hsla(25, 100%, 55%, var(--a, 1));
font: 900 21vw/ 1.25 pacifico, z003, segoe script, comic sans ms, cursive;
}
.base { margin: -.5em 0 0 -.5em; }
.midl {
--a: 0;
backdrop-filter: blur(5px);
-webkit-mask: linear-gradient(red, red) text;
}
.grey { filter: grayscale(1) opacity(.25) }

现在我们开始有所进展了!还有一件事要做:使基础层单色!
/* same as before */
.base {
margin: -.5em 0 0 -.5em;
filter: sepia(1) hue-rotate(165deg) contrast(1.5);
}

好了,这就是我们想要的效果!
获得 Firefox 解决方案
在编写 Chrome 解决方案时,我不禁想到我们也许能够在 Firefox 中获得相同的结果,因为 Firefox 是唯一支持`element()` 函数的浏览器。此函数允许我们获取一个元素并将其用作另一个元素的 `background`。
想法是 `.base` 和 `.grey` 层将具有与 Chrome 版本中相同的样式,而中间层将具有一个 `background`(通过 `element()` 函数),它是我们层的模糊版本。
为了简化操作,我们从这个模糊版本和中间层开始。
<article id='blur' aria-hidden='true'>Lion 🦁</article>
<article class='midl'>Lion 🦁</article>
我们绝对定位模糊版本(现在仍然保持可见),使其单色并模糊,然后将其用作 `.midl` 的 `background`。
#blur {
position: absolute;
top: 2em; right: 0;
margin: -.5em 0 0 -.5em;
filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(5px);
}
.midl {
--a: .5;
background: -moz-element(#blur);
}
我们还使 `.midl` 元素上的文本半透明,以便我们能够透过它看到 `background`。我们最终会将其设置为完全透明,但现在我们仍然想看到它相对于 `background` 的位置。

我们可以立即注意到一个问题:虽然 `margin` 可以偏移实际的 `#blur` 元素,但它对将其作为 `background` 的位置进行偏移没有任何作用。为了获得这种效果,我们需要使用 `transform` 属性。如果我们想要旋转或任何其他 `transform`,这也可以帮助我们——如下所示,我们已将 `margin` 替换为 `transform: rotate(-9deg)`。

好吧,但我们现在仍然坚持只进行平移。
#blur {
/* same as before */
transform: translate(-.25em, -.25em); /* replaced margin */
}

这里需要注意的一点是,当模糊的 `background` 超出中间层 `padding-box` 的限制时,它的一部分会被切掉。无论如何,此步骤中这并不重要,因为我们的下一步是将 `background` 剪裁到文本区域,但拥有该空间很好,因为 `.base` 层将被平移到相同的位置。

因此,我们将稍微增加 `padding`,即使在这一点上,它在视觉上没有任何区别,因为我们还在 `.midl` 元素上设置了 `background-clip: text`。
article {
/* same as before */
padding: .5em;
}
#blur {
position: absolute;
bottom: 100vh;
transform: translate(-.25em, -.25em);
filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(5px);
}
.midl {
--a: .1;
background: -moz-element(#blur);
background-clip: text;
}
我们还将 `#blur` 元素移出了视野,并进一步降低了 `.midl` 元素 `color` 的 alpha 值,因为我们希望更好地透过文本查看 `background`。我们没有使其完全透明,但仍然保持可见,以便我们知道它覆盖的区域。

下一步是添加 `.base` 元素,其样式与 Chrome 案例中的样式几乎相同,只是将 `margin` 替换为 `transform`。
<article id='blur' aria-hidden='true'>Lion 🦁</article>
<article class='base' aria-hidden='true'>Lion 🦁</article>
<article class='midl'>Lion 🦁</article>
#blur {
position: absolute;
bottom: 100vh;
transform: translate(-.25em, -.25em);
filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(5px);
}
.base {
transform: translate(-.25em, -.25em);
filter: sepia(1) hue-rotate(165deg) contrast(1.5);
}
由于这些样式的一部分是通用的,我们还可以将 `.base` 类添加到模糊元素 `#blur` 上,以避免重复并减少我们编写的代码量。
<article id='blur' class='base' aria-hidden='true'>Lion 🦁</article>
<article class='base' aria-hidden='true'>Lion 🦁</article>
<article class='midl'>Lion 🦁</article>
#blur {
--r: 5px;
position: absolute;
bottom: 100vh;
}
.base {
transform: translate(-.25em, -.25em);
filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(var(--r, 0));
}

.base
层后,Firefox的结果截图(在线演示)。这里我们遇到了一个不同的问题。由于.base
层具有transform
属性,因此它现在位于.midl
层的顶部,尽管DOM顺序不同。最简单的解决方法?在.midl
元素上添加z-index: 2
!

.base
位于.midl
下方,Firefox的结果截图(在线演示)。我们还有一个稍微更微妙的问题:.base
元素仍然在我们在.midl
元素上设置的半透明模糊background
的下方可见。我们不希望看到下方.base
层文本的锐利边缘,但我们看到了,因为模糊会导致靠近边缘的像素变得半透明。

根据我们在文本层父元素上设置的background
类型,这个问题可以通过少量或大量的努力来解决。
如果我们只有一个纯色background
,则可以通过将.midl
元素的background-color
设置为相同的值来解决问题。幸运的是,这恰好是我们的情况,因此我们不会深入讨论其他场景。也许会在另一篇文章中讨论。
.midl {
/* same as before */
background: -moz-element(#blur) #fff;
background-clip: text;
}

.base
层不会透过.midl
层的background
可见,Firefox的结果截图(在线演示)。我们离在Firefox中获得一个不错的结果越来越近了!剩下的就是添加顶部的.grey
层,并使用与Chrome版本中完全相同的样式!
.grey { filter: grayscale(1) opacity(.25); }
遗憾的是,这样做并没有产生我们想要的结果,如果我们还将中间层的文本完全设置为透明
(通过将其alpha设置为--a: 0
),那么这一点非常明显,这样我们只能看到其background
(它使用模糊元素#blur
叠加在纯白色
上)裁剪到text
区域。

.grey
层后,Firefox的结果截图(在线演示)。问题是我们看不到.grey
层!由于在它上面设置了z-index: 2
,中间层.midl
现在位于应该位于顶层的元素(.grey
)之上,尽管DOM顺序不同。解决方法?在.grey
层上设置z-index: 3
!
.grey {
z-index: 3;
filter: grayscale(1) opacity(.25);
}
我并不太喜欢一层一层地设置z-index
,但是,它很简单而且有效!我们现在有了一个不错的Firefox解决方案。

将我们的解决方案合并成跨浏览器解决方案
我们从Firefox代码开始,因为它比较多。
<article id='blur' class='base' aria-hidden='true'>Lion 🦁</article>
<article class='base' aria-hidden='true'>Lion 🦁</article>
<article class='midl' aria-hidden='true'>Lion 🦁</article>
<article class='grey'>Lion 🦁</article>
body { display: grid; }
article {
grid-area: 1/ 1;
place-self: center;
padding: .5em;
color: hsla(25, 100%, 55%, var(--a, 1));
font: 900 21vw/ 1.25 cursive;
}
#blur {
--r: 5px;
position: absolute;
bottom: 100vh;
}
.base {
transform: translate(-.25em, -.25em);
filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(var(--r, 0));
}
.midl {
--a: 0;
z-index: 2;
background: -moz-element(#blur) #fff;
background-clip: text;
}
.grey {
z-index: 3;
filter: grayscale(1) opacity(.25);
}
额外的z-index
声明不会影响Chrome中的结果,#blur
元素也不会。为了使其在Chrome中工作,唯一缺少的是.midl
元素上的backdrop-filter
和mask
声明。
backdrop-filter: blur(5px);
-webkit-mask: linear-gradient(red, red) text;
由于我们不希望在Firefox中应用backdrop-filter
,也不希望在Chrome中应用background
,因此我们使用@supports
。
$r: 5px;
/* same as before */
#blur {
/* same as before */
--r: #{$r};
}
.midl {
--a: 0;
z-index: 2;
/* need to reset inside @supports so it doesn't get applied in Firefox */
backdrop-filter: blur($r);
/* invalid value in Firefox, not applied anyway, no need to reset */
-webkit-mask: linear-gradient(red, red) text;
@supports (background: -moz-element(#blur)) { /* for Firefox */
background: -moz-element(#blur) #fff;
background-clip: text;
backdrop-filter: none;
}
}
这给了我们一个跨浏览器解决方案!

虽然两个浏览器中的结果并不完全相同,但仍然非常相似,对我来说已经足够了。
如何将我们的解决方案简化为一个元素?
遗憾的是,这是不可能的。
首先,Firefox解决方案要求我们至少有两个元素,因为我们使用一个(通过其id
引用)作为另一个的background
。
其次,虽然对于剩下的三个层(无论如何这些层是我们Chrome解决方案中唯一需要的),第一个想法是其中一个可以是实际的元素,而另外两个是它的伪元素,但在这种特殊情况下并非如此简单。
对于Chrome解决方案,每个层至少有一个属性也不可逆地影响任何子元素和它可能具有的任何伪元素。对于.base
和.grey
层,这是filter
属性。对于中间层,这是mask
属性。
因此,虽然拥有所有这些元素并不美观,但如果我们希望玻璃质感效果也适用于表情符号,似乎我们没有更好的解决方案。
如果我们只想在纯文本上使用玻璃质感效果——图片中没有表情符号——这可以通过两个元素来实现,其中只有一个是Chrome解决方案所需的。另一个是#blur
元素,我们只在Firefox中需要它。
<article id='blur'>Blood</article>
<article class='text' aria-hidden='true' data-text='Blood'></article>
我们使用.text
元素的两个伪元素来创建底层(使用::before
)和另外两个层的组合(使用::after
)。在这里帮助我们的是,在没有表情符号的情况下,我们不需要filter: grayscale(1)
,而是可以控制color
值的饱和度分量。
这两个伪元素一个叠加在另一个之上,底部的那个(::before
)偏移相同的距离,并具有与#blur
元素相同的color
。此color
值取决于一个标志--f
,它帮助我们控制饱和度和alpha。对于#blur
元素和::before
伪元素(--f: 1
),饱和度为100%
,alpha为1
。对于::after
伪元素(--f: 0
),饱和度为0%
,alpha为.25
。
$r: 5px;
%text { // used by #blur and both .text pseudos
--f: 1;
grid-area: 1/ 1; // stack pseudos, ignored for absolutely positioned #base
padding: .5em;
color: hsla(345, calc(var(--f)*100%), 55%, calc(.25 + .75*var(--f)));
content: attr(data-text);
}
article { font: 900 21vw/ 1.25 cursive }
#blur {
position: absolute;
bottom: 100vh;
filter: blur($r);
}
#blur, .text::before {
transform: translate(-.125em, -.125em);
@extend %text;
}
.text {
display: grid;
&::after {
--f: 0;
@extend %text;
z-index: 2;
backdrop-filter: blur($r);
-webkit-mask: linear-gradient(red, red) text;
@supports (background: -moz-element(#blur)) {
background: -moz-element(#blur) #fff;
background-clip: text;
backdrop-filter: none;
}
}
}
将跨浏览器解决方案应用于我们的用例
这里的好消息是,我们特殊的用例(我们只在链接图标上使用玻璃质感效果,而不是在整个链接(包括文本)上)实际上简化了一些事情。
我们使用以下Pug来生成结构
- let data = {
- home: { ico: '🏠', hue: 200 },
- notes: { ico: '🗒️', hue: 260 },
- activity: { ico: '🔔', hue: 320 },
- discovery: { ico: '🧭', hue: 30 }
- };
- let e = Object.entries(data);
- let n = e.length;
nav
- for(let i = 0; i < n; i++)
- let ico = e[i][1].ico;
a.item(href='#' style=`--hue: ${e[i][1].hue}deg`)
span.icon.tint(id=`blur${i}` aria-hidden='true') #{ico}
span.icon.tint(aria-hidden='true') #{ico}
span.icon.midl(aria-hidden='true' style=`background-image: -moz-element(#blur${i})`) #{ico}
span.icon.grey(aria-hidden='true') #{ico}
| #{e[i][0]}
这会生成如下HTML结构
<nav>
<a class='item' href='#' style='--hue: 200deg'>
<span class='icon tint' id='blur0' aria-hidden='true'>🏠</span>
<span class='icon tint' aria-hidden='true'>🏠</span>
<span class='icon midl' aria-hidden='true' style='background-image: -moz-element(#blur0)'>🏠</span>
<span class='icon grey' aria-hidden='true'>🏠</span>
home
</a>
<!-- the other nav items -->
</nav>
我们可能会用伪元素替换一部分span,但我认为这样更一致也更容易,所以就用span
来做“三明治”吧!
需要注意的一件非常重要的事情是,每个项目都有一个不同的模糊图标层(因为每个项目都有自己的图标),因此我们在style
属性中将.midl
元素的background
设置为它。通过这种方式,如果我们向data
对象添加或删除条目(从而更改菜单项的数量),我们就不需要对CSS文件进行任何更改。
我们几乎拥有了最初使用CSS设置导航栏时相同的布局和美化样式。唯一的区别是,现在我们不再在项目网格的顶部单元格中使用伪元素;我们使用span。
span {
grid-area: 1/ 1; /* stack all emojis on top of one another */
font-size: 4em; /* bump up emoji size */
}
对于表情符号图标层本身,我们也不需要对我们之前获得的跨浏览器版本进行太多更改,尽管有一些小的更改。
首先,我们使用最初在使用链接伪元素而不是span时选择的transform
和filter
链。我们也不再需要span层上的color: hsla()
声明,因为鉴于我们这里只有表情符号,只有alpha通道很重要。默认值(保留用于.base
和.grey
层)为1
。因此,与其设置一个只有alpha--a
通道重要的color
值,并在.midl
层上将其更改为0
,不如直接在其中设置color: transparent
。我们也只需要在Firefox的情况下设置.midl
元素的background-color
,因为我们已经在style
属性中设置了background-image
。这导致了以下解决方案的改编
.base { /* mono emoji version */
transform: translate(.375em, -.25em) rotate(22.5deg);
filter: sepia(1) hue-rotate(var(--hue)) saturate(3) blur(var(--r, 0));
}
.midl { /* middle, transparent emoji version */
color: transparent; /* so it's not visible */
backdrop-filter: blur(5px);
-webkit-mask: linear-gradient(red 0 0) text;
@supports (background: -moz-element(#b)) {
background-color: #fff;
background-clip: text;
backdrop-filter: none;
}
}
就是这样——我们为这个导航栏提供了一个不错的图标玻璃质感效果!

还有一件事需要处理——我们不希望一直都有这种效果;只在:hover
或:focus
状态下。因此,我们将使用一个标志--hl
,在正常状态下为0
,在:hover
或:focus
状态下为1
,以控制.base
span的opacity
和transform
值。这是一种我在之前的文章中详细介绍过的技术。
$t: .3s;
a {
/* same as before */
--hl: 0;
color: hsl(var(--hue), calc(var(--hl)*100%), 65%);
transition: color $t;
&:hover, &:focus { --hl: 1; }
}
.base {
transform:
translate(calc(var(--hl)*.375em), calc(var(--hl)*-.25em))
rotate(calc(var(--hl)*22.5deg));
opacity: var(--hl);
transition: transform $t, opacity $t;
}
当悬停或聚焦图标时,可以在下面的交互式演示中看到结果。
使用SVG图标怎么样?
在CSS表情符号版本完成之后,我自然会问自己这个问题。与span“三明治”相比,使用纯SVG方式是否有意义,它是否更简单?好吧,虽然它确实更有意义,特别是由于我们并非所有内容都使用表情符号,但遗憾的是它并没有减少代码,也没有变得更简单。
但我们将在另一篇文章中详细介绍这一点!
在移动设备上仍然有完整的模糊框效果。
什么操作系统和什么浏览器?
是的,我可以在Android 11 Chrome中确认完整的模糊框,但仅在与嵌入的CodePen交互时。一旦我在它自己的标签页中打开CodePen,效果就会按预期工作。
Android 12也是如此。在新标签页中打开CodePen时效果很好。
非常酷的效果!我有一个问题:为什么使用CSS变量标志而不是将活动状态嵌套在:hover和:focus选择器下?
因为这允许我仅通过更改一个highlight标志来更改多个属性值(在链接本身和内部的span上)。它保持代码简洁且逻辑清晰,它允许我轻松查看悬停/聚焦时修改了哪些属性,而无需查找:hover/:focus块,然后努力同时在屏幕上显示正常状态和:focus/:hover状态以比较感兴趣属性的修改方式。这使得调试速度更快。
我在之前的文章中详细介绍了这一点。