CSS 中的图标玻璃质感效果

Avatar of Ana Tudor
Ana Tudor

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费积分!

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

Animated gif. Shows a nav bar with four grey icons. On :hover/ :focus, a tinted icon slides and rotates, partly coming out from behind the grey one. In the area where they overlap, we have a glassmorphism effect, with the icon in the back seen as blurred through the semitransparent grey one in front.
我们追求的效果。

我关于“几分钟”的判断错得不能再错了——最终我花了几天时间才挠到这个痒痒!

事实证明,虽然有一些资源介绍如何用 CSS 实现这种效果,但它们都假设了一种非常简单的情况,即覆盖层是矩形,或者最多是带 border-radius 的矩形。然而,对于像图标这样的不规则形状(无论是表情符号还是正常的 SVG)实现玻璃质感效果,比我预期的要复杂得多,所以我认为分享一下这个过程、我遇到的陷阱和学到的东西是值得的。还有我仍然不理解的东西。

为什么选择表情符号?

简短回答:因为 SVG 太耗时。详细回答:因为我缺乏在图像编辑器中直接绘制图标的艺术感,但我对语法足够熟悉,以至于我经常可以将在线找到的现成 SVG 压缩到其原始大小的 10% 以下。所以,我不能原封不动地使用从互联网上找到的 SVG——我必须重写代码使其超级简洁和紧凑。而这需要时间。很多时间,因为这是细节工作。

如果我只想快速编写一个带图标的菜单概念,我会求助于使用表情符号,并对其应用滤镜使其与主题相匹配,仅此而已!这就是我在这个 液体标签栏交互 演示中所做的——那些图标都是表情符号!平滑的谷效应利用了 蒙版合成 技术。

Animated gif. Shows a white liquid navigation bar with five items, one of which is selected. The selected one has a smooth valley at the top, with a dot levitating above it. It's also black, while the non-selected ones are grey in the normal state and beige in the :hover/ :focus state. On clicking another icon, the selection smoothly changes as the valley an the levitating dot slide to always be above the currently selected item.
液体导航。

好了,这将是我们的起点:使用表情符号作为图标。

最初的想法

我的第一想法是将导航链接的两个伪元素(带有表情符号内容)叠加,稍微偏移一下,并使用 transform 旋转下面的那个,使它们仅部分重叠。然后,我将使用小于 1opacity 值使上面的那个半透明,在其上设置 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);
  }
}
Screenshot. Shows the four menu items lined up in a row in the middle of the page, each item occupying a column, all columns having the same width; with emojis above the link text, both middle-aligned horizontally.
完成布局基础设置后的 Firefox 屏幕截图。

请注意,表情符号的外观会因您用来查看演示的浏览器而异。

我们选择一个易读的 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;
  }
}
Screenshot. Shows the same layout as before, only with a prettier and bigger font and even bigger icons, backgrounds and each menu item having a different color value based on its --hue.
Chrome 屏幕截图(实时演示),美化了一些内容。

从这里开始,事情变得有趣起来,因为我们开始区分使用链接伪元素创建的两个表情符号层。我们稍微移动并旋转 ::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);
  }
}
Screenshot. Same nav bar as before, only now the top icon layer is grey and semitransparent, while the bottom one is slightly offset and rotated, mono in the specified --hue.
Chrome 屏幕截图(实时演示),区分了两个图标层。

遇到障碍

到目前为止,一切都很好……所以呢?下一步,当我想到编写这个代码时,我愚蠢地认为这将是最后一步,它涉及到在顶部(::after)层上设置 backdrop-filter: blur(5px)

请注意,Firefox 仍然需要将 about:config 中的 gfx.webrender.alllayout.css.backdrop-filter.enabled 标志设置为 true,才能使 backdrop-filter 属性生效。

Animated gif. Shows how to find the flags mentioned above (gfx.webrender.all and layout.css.backdrop-filter.enabled) in order to ensure they are set to true. Go to about:config, start typing their name in the search box and double click their value to change it if it's not set to true already.
Firefox 中仍然需要的,才能使 backdrop-filter 生效的标志。

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

Screenshot collage. Shows the not really blurred, but awkward result with an overlay the size of the top emoji box after applying the backdrop-filter property. This happens both in Chrome (top) and in Firefox (bottom).
Chrome(顶部)和 Firefox(底部)屏幕截图(实时演示),应用 backdrop-filter 后。

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

Screenshot. Shows a working glassmorphism effect, created via a control panel where we draw some sliders to get the value for each filter function.
在我之前编写的一个演示中,有效的玻璃质感效果(实时演示)。

找到问题的根源

好吧,当你完全不知道为什么某些东西不起作用时,你所能做的就是拿另一个有效的示例,开始对其进行调整以尝试获得你想要的结果……然后看看它在哪里崩溃!

因此,让我们看看我之前有效的演示的简化版本。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);
}
Screenshot. Shows a working glassmorphism effect, where we have a semitransparent box on top of its parent one, having an image background.
简化测试中的有效玻璃质感效果(实时演示)。

这有效,但我们不希望我们的两层嵌套在一起;我们希望它们成为兄弟元素。因此,让我们将两层都设为 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);
}
Screenshot collage. Shows the case where we have a semitransparent box on top of its sibling having an image background. The top panel screenshot was taken in Chrome, where the glassmorphism effect works as expected. The bottom panel screenshot was taken in Firefox, where things are mostly fine, but the blur handling around the edges is really weird.
两层为兄弟元素时的 Chrome(顶部)和 Firefox(底部)屏幕截图(实时演示)。

在 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%);
}
Screenshot. Shows the case where we have a subunitary opacity on the top layer in order to make it semitransparent, instead of a subunitary alpha value for the semitransparent background.
使用 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);
}
Screenshot collage. Shows the case where we have a semitransparent text layer on top of its identical solid orange text sibling. The top panel screenshot was taken in Chrome, where we get proper blurring, but it's underneath the entire bounding box of the semitransparent top text, not limited to just the actual text. The bottom panel screenshot was taken in Firefox, where things are even worse, with the blur handling around the edges being really weird.
当我们有两个文本层时,结果的 Chrome(顶部)和 Firefox(底部)屏幕截图(实时演示)。

这里需要注意几点。

首先,**将 `color` 属性设置为具有子单位 alpha 的值也会使表情符号半透明,而不仅仅是纯文本**,这在 Chrome 和 Firefox 中都是如此!这是我以前从未知道的事情,我发现它绝对令人难以置信,因为其他通道根本不会影响表情符号。

其次,Chrome 和 Firefox 都模糊了橙色文本和表情符号在顶部半透明灰色层边界框下方的整个区域,而不是只模糊实际文本下方的内容。在 Firefox 中,由于那个笨拙的锐利边缘效果,情况看起来更糟。

即使框模糊不是我们想要的,但我还是忍不住认为它是有道理的,因为规范确实说了以下内容

[...]要创建一个“透明”元素以显示完整的过滤背景图像,可以使用“background-color: transparent;”。

因此,让我们做一个测试来检查当顶层是另一个非矩形形状(不是文本,而是通过 `background` 渐变、`clip-path` 或 `mask` 获得)时会发生什么!

Screenshot collage. Shows the case where we have semitransparent non-rectangular shaped layers (obtained with three various methods: gradient background, clip-path and mask) on top of a rectangular siblings. The top panel screenshot was taken in Chrome, where things seem to work fine in the clip-path and mask case, but not in the gradient background case. In this case, everything that's underneath the bounding box of the top element gets blurred, not just what's underneath the visible part. The bottom panel screenshot was taken in Firefox, where, regardless of the way we got the shape, everything underneath its bounding box gets blurred, not just what's underneath the actual shape. Furthermore, in all three cases we have the old awkward sharp edge issue we've had in Firefox before
当顶层是非矩形形状时,结果的 Chrome(顶部)和 Firefox(底部)屏幕截图(实时演示)。

在 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 */
}
Chrome screenshot. Shows two text and emoji layers partly overlapping. The top one is semitransparent, so through it, we can see the layer underneath blurred (by applying a backdrop-filter on the top one).
当顶层具有限制在文本区域的遮罩时,结果的 Chrome 屏幕截图(实时演示)。

好吧,这实际上看起来像是我们想要的,所以我们可以说我们正在朝着正确的方向前进!但是,这里我们使用了橙色心形表情符号作为底层,黑色心形表情符号作为顶部半透明层。其他通用表情符号没有黑白版本,所以我的下一个想法是最初使两层相同,然后使顶层半透明并在其上使用 `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;
}
Chrome screenshot. Shows two text and emoji layers partly overlapping. The top one is semitransparent, so through it, we can see the layer underneath blurred (by applying a backdrop-filter on the top one). The problem is that applying the grayscale filter on the top semitransparent layer not only affects this layer, but also the blurred area of the layer underneath.
当顶层获得 `grayscale(1)` 过滤器时,结果的 Chrome 屏幕截图(实时演示)。

好吧,这确实对顶层产生了我们想要的效果。不幸的是,由于某种奇怪的原因,它似乎也影响了下方层的模糊区域。这一刻是短暂考虑将笔记本电脑扔出窗外的时候……然后想到再添加一层。

它将是这样的:我们有基础层,就像我们到目前为止所做的那样,稍微偏离上面另外两个层。中间层是一个“幽灵”(透明)层,应用了 `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) }
Chrome screenshot. Shows two text and emoji layers partly overlapping. The top one is semitransparent grey, so through it, we can see the layer underneath blurred (by applying a backdrop-filter on a middle, completely transparent one).
使用三层的结果的 Chrome 屏幕截图(实时演示)。

现在我们开始有所进展了!还有一件事要做:使基础层单色!

/* same as before */

.base {
  margin: -.5em 0 0 -.5em;
  filter: sepia(1) hue-rotate(165deg) contrast(1.5);
}
Chrome screenshot. Shows two text and emoji layers partly overlapping. The bottom one is mono (bluish in this case) and blurred at the intersection with the semitransparent grey one on top.
我们想要的最终结果的 Chrome 屏幕截图(实时演示)。

好了,这就是我们想要的效果!

获得 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` 的位置。

Firefox screenshot. Shows a blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the blurred element via the element() function.
使用模糊元素 `#blur` 作为 `background` 时的 Firefox 屏幕截图(实时演示)。

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

Firefox screenshot. Shows a slightly rotated blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the slightly rotated blurred element via the element() function.
在 `#blur` 元素上使用 `transform: rotate()` 代替 `margin` 时的 Firefox 屏幕截图(实时演示)。

好吧,但我们现在仍然坚持只进行平移。

#blur {
  /* same as before */
  transform: translate(-.25em, -.25em); /* replaced margin */
}
Firefox screenshot. Shows a slightly offset blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the slightly offset blurred element via the element() function. This slight offset means the actual text doesn't perfectly overlap with the background one anymore.
在 `#blur` 元素上使用 `transform: translate()` 代替 `margin` 时的 Firefox 屏幕截图(实时演示)。

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

Firefox screenshot. Shows a slightly offset blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the slightly offset blurred element via the element() function. This slight offset means the actual text doesn't perfectly overlap with the background one anymore. It also means that the translated background text may not fully be within the limits of the padding-box anymore, as highlighted in this screenshot, which also shows the element boxes overlays.
Firefox 屏幕截图突出显示平移的 `#blur` 背景如何超出 `.midl` 元素的 `padding-box` 限制。

因此,我们将稍微增加 `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`。我们没有使其完全透明,但仍然保持可见,以便我们知道它覆盖的区域。

Firefox screenshot. Shows a text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to a blurred element (now positioned out of sight) via the element() function. This slight offset means the actual text doesn't perfectly overlap with the background one anymore. We have also clipped the background of this element to the text, so that none of the background outside it is visible. Even so, there's enough padding room so that the blurred background is contained within the padding-box.
在将 `.midl` 元素的 `background` 剪裁到 `text` 之后的结果的 Firefox 屏幕截图(实时演示)。

下一步是添加 `.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));
}
Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has semitransparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. In spite of DOM order, the .base layer still shows up on top.
添加.base层后,Firefox的结果截图(在线演示)。

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

Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has semitransparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. Having explicitly set a z-index on the .midl layer, it now shows up on top of the .base one.
修复层级顺序后,使.base位于.midl下方,Firefox的结果截图(在线演示)。

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

Screenshot. Shows two lines of blue text with a red outline to highlight the boundaries of the actual text. The text on the second line is blurred and it can be seen how this causes us to have semitransparent blue pixels on both sides of the red outline - both outside and inside.
边缘周围的模糊效果。

根据我们在文本层父元素上设置的background类型,这个问题可以通过少量或大量的努力来解决。

如果我们只有一个纯色background,则可以通过将.midl元素的background-color设置为相同的值来解决问题。幸运的是,这恰好是我们的情况,因此我们不会深入讨论其他场景。也许会在另一篇文章中讨论。

.midl {
  /* same as before */
  background: -moz-element(#blur) #fff;
  background-clip: text;
}
Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has semitransparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. Having explicitly set a z-index on the .midl layer and having set a fully opaque background-color on it, the .base layer now lies underneath it and it isn't visible through any semitransparent parts in the text area because there aren't any more such parts.
确保.base层不会透过.midl层的background可见,Firefox的结果截图(在线演示)。

我们离在Firefox中获得一个不错的结果越来越近了!剩下的就是添加顶部的.grey层,并使用与Chrome版本中完全相同的样式!

.grey { filter: grayscale(1) opacity(.25); }

遗憾的是,这样做并没有产生我们想要的结果,如果我们还将中间层的文本完全设置为透明(通过将其alpha设置为--a: 0),那么这一点非常明显,这样我们只能看到其background(它使用模糊元素#blur叠加在纯白色上)裁剪到text区域。

Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has transparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. Since the background-color of this layer coincides to that of their parent, it is hard to see. We also have a third .grey layer, the last in DOM order. This should be right on top of the .midl one, but, due to having set a z-index on the .midl layer, the .grey layer is underneath it and not visible, in spite of the DOM order.
添加顶部的.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 screenshot. Shows two text and emoji layers partly overlapping. The bottom one is mono (bluish in this case) and blurred at the intersection with the semitransparent grey one on top.
我们想要的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-filtermask声明。

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;
  }
}

这给了我们一个跨浏览器解决方案!

Chrome (top) and Firefox (bottom) screenshot collage of the text and emoji glassmorphism effect for comparison. The blurred backdrop seems thicker in Chrome and the emojis are obviously different, but the result is otherwise pretty similar.
我们想要的Chrome(顶部)和Firefox(底部)结果截图(在线演示)。

虽然两个浏览器中的结果并不完全相同,但仍然非常相似,对我来说已经足够了。

如何将我们的解决方案简化为一个元素?

遗憾的是,这是不可能的。

首先,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时选择的transformfilter链。我们也不再需要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;
  }
}

就是这样——我们为这个导航栏提供了一个不错的图标玻璃质感效果!

Chrome (top) and Firefox (bottom) screenshot collage of the emoji glassmorphism effect for comparison. The emojis are obviously different, but the result is otherwise pretty similar.
我们想要的表情符号玻璃质感效果的Chrome(顶部)和Firefox(底部)截图(在线演示)。

还有一件事需要处理——我们不希望一直都有这种效果;只在:hover:focus状态下。因此,我们将使用一个标志--hl,在正常状态下为0,在:hover:focus状态下为1,以控制.basespan的opacitytransform值。这是一种我在之前的文章中详细介绍过的技术。

$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方式是否有意义,它是否更简单?好吧,虽然它确实更有意义,特别是由于我们并非所有内容都使用表情符号,但遗憾的是它并没有减少代码,也没有变得更简单。

但我们将在另一篇文章中详细介绍这一点!