CSS中的图标Glassmorphism效果

122 阅读8分钟

我最近在Dribble的一个镜头中看到了一个很酷的效果,叫做玻璃形态。我的第一个想法是,如果我只是用一些表情符号来做图标的话,我可以在几分钟内快速地重现它,而不用浪费时间去做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,获得玻璃变形效果比我想象的要复杂得多,所以我认为值得分享这个过程、我陷入的陷阱以及我在这个过程中学到的东西。还有那些我仍然不明白的事情。

为什么是emojis?

简短的回答:因为SVG需要太多的时间。长答案:因为我缺乏在图像编辑器中绘制它们的艺术感,但我对语法很熟悉,以至于我经常可以把我在网上找到的现成的SVG压缩到其原始尺寸的10%以下。所以,我不能只是使用我在网上找到的它们--我必须重做代码,使其超级干净和紧凑。而这需要时间。很多时间,因为它是细节工作。

如果我只想快速编写一个带有图标的菜单概念,我就会使用表情符号,对它们进行过滤,以使它们与主题相匹配,就这样!这就是我为这个项目所做的。这就是我为这个液体标签栏互动演示所做的--那些图标都是emojis!平滑的山谷效果利用了蒙版合成技术。

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.

液体导航。

好的,所以这将是我们的起点:用emojis做图标。

最初的想法

我的第一个想法是把导航链接的两个假象(有表情符号的内容)堆叠起来,稍微偏移,用transform ,旋转底部的假象,使它们只有部分重叠。opacity 然后,我将使最上面的一个半透明,其值小于1 ,在其上设置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.

火狐浏览器的截图是我们整理了布局的基本要素后的结果。

请注意,表情符号的外观会因你使用的浏览器查看演示而有所不同。

我们挑选了一个可读的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仍然需要将gfx.webrender.alllayout.css.backdrop-filter.enabled 标志设置为trueabout:config ,以便使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只是一个article ,在section 。在CSS中,我们首先设置了一些尺寸,然后我们在section ,在article ,设置了一个半透明的图片background 。最后,我们在文章上设置了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 ,让它们部分重叠,看看我们的glassmorphism效果是否仍然有效。

<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 的模糊半径的两倍。当然,在我们的特殊情况下,这个修正似乎是有效的。

如果我们的两个图层位于一个有图像的元素上,那么事情就会变得更加棘手,backgroundfixed(在这种情况下,我们可以使用分层背景的方法来解决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() 背景时,结果(现场演示)是半透明的。

看起来我们找到了问题所在!由于某些原因,在Chrome和Firefox中使用opacity 使顶层半透明会破坏backdrop-filter 的效果。这是个错误吗?那是应该发生的吗?

到底是不是错误?

MDN在 backdrop-filter 页面的第一段中说了以下内容。

因为它适用于元素后面的所有东西,所以要看到这个效果,你必须使该元素或其背景至少部分透明。

除非我没有理解上面这句话,否则这似乎是在暗示opacity ,不应该破坏这个效果,尽管它在Chrome和Firefox中都是如此。

那么规范呢?好吧,该规范是一堵巨大的文字墙,没有很多插图或互动演示,用一种让人读起来像闻臭鼬的气味腺一样有吸引力的语言写的。它包含这个部分,我感觉它可能是相关的,但我不确定我是否理解它想说的是什么--在顶部元素上设置的opacity ,我们也有backdrop-filter ,也会被应用到它下面的兄弟姐妹身上?如果这是预期的结果,在实践中肯定不会发生。

除非元素B的某些部分是半透明的,否则背景滤镜的效果将不可见。还要注意的是,应用于元素B的任何不透明性也将应用于过滤后的背景图像。

尝试随机的东西

不管规范是怎么说的,事实是:用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(底部)的结果截图(现场演示)。

这里有几件事情需要注意。

首先,在Chrome和Firefox中,color 属性设置为具有亚单位阿尔法的值也会使表情符号半透明,而不仅仅是纯文本。这是我以前从来不知道的事情,而且我发现这绝对是令人难以置信的,因为其他通道不会以任何方式影响表情符号。

第二,Chrome和Firefox都在模糊整个橙色文本和表情符号的区域,这些区域位于顶部半透明灰色层的边界框下面,而不是仅仅模糊实际文本下面的内容。在火狐浏览器中,由于那个尴尬的尖锐边缘效果,事情看起来甚至更糟。

尽管盒式模糊不是我们想要的,但我不禁认为它确实是有意义的,因为规范中确实提到了以下几点。

[......]要创建一个 "透明 "元素,让人们看到完整的过滤后的背景图像,你可以使用 "background-color: transparent;"。

因此,让我们做个测试,看看当顶层是另一个非矩形的形状,而不是文字,而是用background 渐变、clip-pathmask !会发生什么?

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-pathmask 形状,而Firefox则不尊重。而且,在这种情况下,我真的不知道哪个是正确的,尽管Chrome的结果对我来说更有意义。

逐步采用Chrome浏览器的解决方案

这个结果和我在Twitter上得到的一个建议,当我问到如何使模糊尊重文本边缘而不是其边界框的边缘时,我想到了Chrome的下一步:在顶层应用一个mask ,并将其剪切到text (.grey)。这个解决方案在Firefox中不起作用,原因有二:第一,text 是一个非标准的 mask-clip值,而且,正如上面的测试所显示的,无论如何,蒙版不会将模糊区域限制在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.

Chrome浏览器截图,当顶层得到一个grayscale(1) 过滤器时,结果(现场演示)。

好吧,这当然有我们想要的顶层的效果。不幸的是,由于一些奇怪的原因,它似乎也影响了下面那层的模糊区域。这一刻,我短暂地考虑把笔记本扔出窗外......然后才有了再加一层的想法。

它将是这样的:我们有一个基础层,就像我们到目前为止所做的那样,与上面的其他两个层稍稍偏移。中间层是一个 "幽灵"(透明)层,应用了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屏幕截图。

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

达成火狐浏览器的解决方案

在编写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 时的结果截图(实时演示)。

我们可以马上注意到一个问题:虽然margin 可以偏移实际的#blur 元素,但它对移动其作为background 的位置没有任何作用。为了得到这样的效果,我们需要使用transform 属性。如果我们想要一个旋转或任何其他的transform ,这也可以帮助我们--就像下面我们用transform: rotate(-9deg) 来替换margin

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 时的结果截图(实时演示)。

好吧,但我们现在仍然坚持只做翻译。

#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 剪切到text 区域,但有这样的空间是很好的,因为.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.

火狐浏览器的截图强调了翻译后的#blur 背景如何超过了padding-box.midl 元素上的限制。

所以,我们要把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 后的结果截图(实时演示)。

下一步是添加.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);
}

由于这些样式中有一部分是通用的,我们也可以在我们的模糊元素#blur 上添加.base 类,以避免重复,并减少我们的代码量。

<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 层后的结果(实时演示)。

我们在这里有一个不同的问题。由于.base 层有一个transform,所以尽管有 DOM 顺序,它现在还是在.midl 层的上面。最简单的解决方法?在.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 的下面之后,结果的截图(实时演示)。

我们还有另一个稍微微妙的问题:.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 层不通过background.midl 层可见之后。

在Firefox中,我们已经接近一个不错的结果了!剩下要做的就是用与Chrome版本完全相同的样式添加顶部的.grey 层!

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

遗憾的是,这样做并不能产生我们想要的结果,如果我们也让中间层的文字完全transparent (通过将其alpha归零--a: 0 ),这样我们就只能看到其background (在实心的white 上使用模糊的元素#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 图层后的结果截图(实时演示)。

问题是我们无法看到.grey 层!由于在它上面设置了z-index: 2 ,中间层.midl 现在在应该是顶层(.grey )的上面,尽管有DOM顺序。如何解决?将z-index: 3 设置在.grey 层上!

.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浏览器中工作,唯一缺少的是backdrop-filtermask.midl 元素上的声明。

backdrop-filter: blur(5px);
-webkit-mask: linear-gradient(red, red) text;

由于我们不希望backdrop-filter 在Firefox中被应用,也不希望background 在Chrome中被应用,我们使用@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 属性。

因此,虽然拥有所有这些元素并不漂亮,但如果我们想让glassmorphism效果在emojis上也起作用的话,看起来我们没有更好的解决方案。

如果我们只想在纯文本上实现玻璃形态效果--图片中没有表情符号--这只需要两个元素就可以实现,其中只有一个是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 ,它帮助我们控制饱和度和阿尔法。对于#blur 元素和::before 伪元素 (--f: 1) ,饱和度是100% ,阿尔法是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;
    }
  }
}

CodePen 嵌入回退

将跨浏览器解决方案应用于我们的用例

好消息是,我们的特殊用例是,我们只在链接图标上有玻璃形态效果(而不是在整个链接上,包括文字),这实际上简化了一点点事情。

我们使用下面的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 三明治吧!

background 有一件非常重要的事情需要注意,我们为每一个项目设置了不同的模糊图标层(因为每一个项目都有自己的图标),所以我们在style 属性中把.midl 元素设置为它。这样做可以让我们避免在添加或删除data 对象的条目(从而改变菜单项的数量)时对CSS文件做任何修改。

我们几乎拥有与我们第一次对导航栏进行CSS编辑时相同的布局和美化的样式。唯一的区别是,现在我们在项目网格的顶部单元格中没有假体;我们有跨度。

span {
  grid-area: 1/ 1; /* stack all emojis on top of one another */
  font-size: 4em; /* bump up emoji size */
}

对于emoji图标层本身,我们也不需要从我们早些时候得到的跨浏览器版本中做很多改变,尽管有一些小的改变。

首先,我们使用transformfilter 链,这是我们最初使用链接假体而不是跨度时选择的。我们也不再需要跨度层上的color: hsla() 声明,因为鉴于我们这里只有表情符号,只有alpha通道是重要的。默认情况下,.base.grey 图层保留的是1 。因此,我们不需要设置一个color ,其中只有alpha,--a ,通道是重要的,我们在.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(底部)所需的emoji glassmorphism效果的屏幕截图(现场演示)。

还有一件事要注意--我们不希望在任何时候都有这种效果;只在:hover:focus 状态。.base 因此,我们要使用一个标志,--hl ,在正常状态下是0 ,在:hover:focus 状态下是1 ,以便控制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;
}

当图标被悬停或聚焦时,其结果可以在下面的互动演示中看到。

CodePen嵌入回退

那么使用SVG图标呢?

我很自然地问了自己这个问题,因为我花了很多时间才让CSS表情符号版本正常工作。普通的SVG方式不是比span夹层更有意义吗,不是更简单吗?好吧,虽然它确实更有意义,尤其是我们并不是所有的东西都有表情符号,但遗憾的是,它的代码并没有减少,也没有更简单。