CSS 糖果幽灵按钮

Avatar of Ana Tudor
Ana Tudor on

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

最近,在寻找一些关于如何编码的思路时,因为我一点艺术感都没有,所以我唯一能做的就是找到其他人的漂亮作品,然后用干净紧凑的代码重新制作它们…… 我偶然发现了 这些糖果幽灵按钮

它们似乎是我可以快速编码的酷炫小东西的完美选择。 不到 15 分钟后,这就是我的 Chromium 结果

Chrome screenshot. Shows a four row, five column grid of candy ghost buttons with text and an icon following it. These buttons have an elongated pill-like shape, a transparent background and a continuous sweet pastel gradient for the border and the text and icon inside.
纯 CSS 糖果幽灵按钮。

我认为这项技术值得分享,因此在这篇文章中,我们将逐步介绍我最初是如何做到的,以及我们还有哪些其他选择。

起点

一个按钮是用… 你准备好这个了吗? 一个 button 元素! 这个 button 元素有一个 data-ico 属性,我们在这个属性中添加一个表情符号。 它还有一个停止列表自定义属性 --slist,它在 style 属性中设置。

<button data-ico="👻" style="--slist: #ffda5f, #f9376b">boo!</button>

在写完这篇文章后,我了解到 Safari 在剪切到 text 方面存在许多问题,即它不适用于 button 元素,也不适用于 具有 display: flex 的元素(可能还有 grid?)以及 元素子级中的文本。 可悲的是,这意味着此处介绍的所有技术在 Safari 中都无法使用。 唯一的解决方法是将这里的所有 button 样式应用于嵌套在 button 内部的 span 元素,覆盖其父级的 border-box。 并且,如果这能帮助其他像我一样在 Linux 上没有物理访问 Apple 设备的人(除非你算上最近第四层楼上的人买的 iPhone 5 - 你不想在超过一个月两次以上的情况下用这种东西烦扰他们 - ),我还了解到将来要使用 Epiphany。 感谢 Brian 的建议!

对于 CSS 部分,我们在一个 ::after 伪元素中添加图标,并在 button 上使用 grid 布局,以便文本和图标都具有良好的对齐方式。 在 button 上,我们还设置了 borderpaddingborder-radius,使用停止列表 --slist 来创建一个对角线渐变,并美化 font

button {
  display: grid;
  grid-auto-flow: column;
  grid-gap: .5em;
  border: solid .25em transparent;
  padding: 1em 1.5em;
  border-radius: 9em;
  background: 
    linear-gradient(to right bottom, var(--slist)) 
      border-box;
  font: 700 1.5em/ 1.25 ubuntu, sans-serif;
  text-transform: uppercase;
  
  &::after { content: attr(data-ico) }
}

关于上面的代码需要澄清的一点。 在突出显示的行中,我们将 background-originbackground-clip 都设置为 border-boxbackground-originbackground-position0 0 点放在它设置到的框的左上角,并提供框的尺寸,background-size 是相对于这个框的尺寸。

也就是说,如果 background-origin 设置为 padding-box,则 background-position0 0 点位于 padding-box 的左上角。 如果 background-origin 设置为 border-box,则 background-position0 0 点位于 border-box 的左上角。 如果 background-origin 设置为 padding-box,则 background-size50% 25% 意味着 padding-box 宽度为 50%padding-box 高度为 25%。 如果 background-origin 设置为 border-box,则相同的 background-size50% 25% 意味着 border-box 宽度为 50%border-box 高度为 25%

background-origin 的默认值为 padding-box,这意味着默认尺寸为 100% 100% 的渐变将覆盖 padding-box,然后在 border 下面重复自身(如果 border 完全不透明,我们看不到它)。 但是,在我们的案例中,border 完全 transparent,我们希望我们的渐变扩展到整个 border-box。 这意味着我们需要将 background-origin 值更改为 border-box

Screenshot collage. Chrome on the left, Firefox on the right, showing differences between ghost emojis. The button has a pastel gradient background going along the main diagonal, the text 'Boo!' in black and a ghost emoji, which is going to look different depending on the OS and browser.
应用基本样式后的结果 (实时演示)。

简单但遗憾的是非标准的 Chromium 解决方案

这涉及使用三个 mask 层并进行合成。 如果您需要关于 mask 合成的复习,您可以查看 这个速成课程

请注意,在 CSS mask 层的情况下,只有 alpha 通道很重要,因为蒙版元素的每个像素都会获得相应 mask 像素的 alpha,而 RGB 通道不会以任何方式影响结果,因此它们可以是任何有效的价值。 在下面,您可以看到紫色到透明渐变叠加的效果与使用完全相同的渐变作为 mask 的效果的对比。

Screenshot. Shows two Halloween-themed cat pictures (the cat is protectively climbed on top of a Halloween pumpkin) side by side. The first one has a purple to transparent linear gradient overlay on top. The second one uses the exact same linear gradient as a mask. By default, CSS masks are alpha masks, meaning that every pixel of the masked element gets the alpha of the corresponding mask pixel.
渐变叠加与相同渐变蒙版 (实时演示)。

我们将从最底部的两层开始。 第一个是一个完全不透明的层,完全覆盖整个 border-box,这意味着它在任何地方都有 1 的 alpha。 另一个也是完全不透明的,但被限制在 (通过使用 mask-clip) padding-box,这意味着尽管这个层在整个 padding-box 中都有 1 的 alpha,但在 border 区域它完全 transparent,在那里 alpha 为 0

如果您难以想象这一点,一个好的技巧是将元素的布局框视为嵌套的矩形,就像下面所示的那样。

Illustration showing the layout boxes. The outermost box is the border-box. Inside it, a border-width away from the border limit, we have the padding-box. And finally, inside the padding-box, a padding away from the padding limit, we have the content-box.
布局框 (实时演示)。

在我们的案例中,底层在整个橙色框 (border-box) 中完全不透明 (alpha 值为 1)。 我们放在最上面第二层在红色框 (padding-box) 的整个区域中完全不透明 (alpha 值为 1),并且在 padding 限制和 border 限制之间的区域中完全 transparent (alpha 为 0)。

关于这些框的限制的一个非常酷的事情是,它们的圆角由 border-radius 确定 (在 padding-box 的情况下,也由 border-width 确定)。 这一点在下面的交互式演示中得到说明,在那里我们可以看到 border-box 的圆角由 border-radius 值给出,而 padding-box 的圆角计算为 border-radius 减去 border-width (如果差值为负值,则限制在 0)。

现在让我们回到我们的 mask 层,一层在整个 border-box 中完全不透明,而最上面的一层在 padding-box 区域中完全不透明,并且在 padding 限制和 border 限制之间的 border 区域中完全透明。 这两层使用 exclude 操作 (在 非标准 WebKit 版本中称为 xor) 进行合成。

Illustration. Shows the bottom two background layers in 3D. The first one from the bottom has an alpha of 1 all across the entire border-box. The second one, layered on top of it, has an alpha of 1 across the padding box, within the padding limit; it also has an alpha of 0 in the border area, outside the padding limit, but inside the border limit.
两个基本层 (实时演示)。

在我们的案例中,这两层 alpha 都为 01,这个操作的名称在这方面非常有提示性 - 第一层的 alpha 在任何地方都为 1,而第二层的 alpha (我们放在第一层之上) 在 padding-box 内为 1,在 padding 限制和 border 限制之间的 border 区域中为 0

在这种情况下,布尔逻辑规则 的应用非常直观 - 对两个相同的值进行 XOR 操作将得到 0,而对两个不同的值进行 XOR 操作将得到 1

在整个 padding-box 中,第一层和第二层都有 1 的 alpha,因此使用这个操作对它们进行合成,在这个区域中,结果层的 alpha 为 0。 但是,在 border 区域 (在 padding 限制之外,但在 border 限制之内),第一层的 alpha 为 1,而第二层的 alpha 为 0,因此在这个区域中,结果层的 alpha 为 1

这一点在下面的交互式演示中得到说明,在那里您可以切换查看在 3D 中分离的两个 mask 层,以及查看使用此操作叠加和合成的两个 mask 层。

将这些内容放入代码中,我们有

button {
  /* same base styles */
  --full: linear-gradient(red 0 0);
  -webkit-mask: var(--full) padding-box, var(--full);
  -webkit-mask-composite: xor;
  mask: var(--full) padding-box exclude, var(--full);
}

在我们继续之前,让我们讨论一下关于上面 CSS 的一些微调细节。

首先,由于完全不透明的层可以是任何东西 (alpha 通道是固定的,始终为 1,RGB 通道无关紧要),我通常将它们设置为 red - 只有三个字符! 同样,使用圆锥形渐变而不是线性渐变也能为我们节省一个字符,但我很少这样做,因为我们仍然有支持 遮罩不支持圆锥形渐变 的移动浏览器。 使用线性渐变可以确保我们有全面的支持。 好吧,除了 IE 和 pre-Chromium Edge 之外,但话说回来,在这些浏览器中,也没有太多酷炫的东西可以正常使用。

其次,我们对两层都使用了渐变。 我们没有对最底层使用纯 background-color,因为我们无法为 background-color 本身设置单独的 background-clip。 如果我们将 background-image 层剪切到 padding-box,则这个 background-clip 值也适用于下面的 background-color - 它也会被剪切到 padding-box,我们将无法让它覆盖整个 border-box

第三,我们没有显式地为底层设置 `mask-clip` 值,因为此属性的默认值正是我们在此情况下想要的值,即 `border-box`。

第四,我们可以将标准的 `mask-composite`(Firefox 支持)包含在 `mask` 简写中,但不能包含非标准的(WebKit 浏览器支持)。

最后,我们始终将标准版本放在最后,以便它覆盖任何可能也受支持的非标准版本。

到目前为止,我们的代码的结果(目前仍然是跨浏览器的)如下所示。我们还在根元素上添加了一个 `background-image`,以便明显地看到我们在整个 `padding-box` 区域拥有真正的透明度。

Screenshot. The pastel gradient button is just a shadow of its former self. Well, just a border, that's all we can see of it. The entire area inside the padding limit has been masked out and we can now see through to the image background behind the button.
在将整个 `padding-box` 遮罩掉之后的结果(实时演示)。

这不是我们想要的。虽然我们有一个漂亮的渐变 `border`(顺便说一下,这是我首选的获取渐变 `border` 的方法,因为我们在整个 `padding-box` 区域拥有真正的透明度,而不仅仅是 一个覆盖),但现在文本不见了。

所以下一步是使用另一个 `mask` 层在之前的层之上添加回文本,这次是一个限制在 `text` 范围内的层(同时使实际文本完全 `transparent`,以便我们可以看到渐变 `background` 穿透它),并将这个第三个 `mask` 层与前两个 `mask` 层的异或结果进行异或(结果可以在上面的截图中看到)。

下面的交互式演示允许以 3D 分离和堆叠和合成的方式查看三个 `mask` 层。

请注意,`mask-clip` 的 `text` 值是非标准的,所以不幸的是,这只在 Chrome 中有效。在 Firefox 中,我们不再对按钮进行任何遮罩,并且由于文本已设置为 `transparent`,我们甚至没有获得优雅的降级。

button {
  /* same base styles */
  -webkit-text-fill-color: transparent;
  --full: linear-gradient(red 0 0);
  -webkit-mask: var(--full) text, var(--full) padding-box, var(--full);
  -webkit-mask-composite: xor;
  /* sadly, still same result as before :( */
  mask: var(--full) padding-box exclude, var(--full);
}

如果我们不想以这种方式使我们的 `button` 不可用,我们应该将应用 `mask` 和使文本 `transparent` 的代码放在一个 `@supports` 块中。

button {
  /* same base styles */

  @supports (-webkit-mask-clip: text) {
    -webkit-text-fill-color: transparent;
    --full: linear-gradient(red 0 0);
    -webkit-mask: var(--full) text, var(--full) padding-box, var(--full);
    -webkit-mask-composite: xor;
  }
}
Screenshot collage. Chrome (left) vs. Firefox (right). In Chrome, we have a real pill-shaped pastel gradient ghost button. It has a transparent background that lets us see through to the image background behind our button and a continuous sweet pastel gradient for the border and the text and icon inside. In Firefox, we have the same pill-shaped, pastel background, black text and normal emoji button we had after setting the base styles. The ghost emoji is going to look different depending on the OS and browser - here it can be seen it has different looks in Chrome and Firefox.
使用仅遮罩方法的最终结果(实时演示)。

我真的很喜欢这种方法,它是我们目前拥有的最简单的方法,我真的很希望 `text` 是 `mask-clip` 的一个标准值,并且所有浏览器都正确地支持它。

但是,我们还有几种其他方法可以实现糖果幽灵按钮效果,虽然它们要么比我们刚刚讨论的非标准 Chromium 独有方法更复杂,要么更受限制,但它们也得到了更好的支持。所以让我们看一下这些方法。

额外的伪元素解决方案

这涉及设置与之前相同的初始样式,但我们没有使用 `mask`,而是将 `background` 剪切到 `text` 区域。

button {
  /* same base styles */
  background: 
    linear-gradient(to right bottom, var(--slist)) 
    border-box;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent
}

就像之前一样,我们还需要使实际文本 `transparent`,以便我们可以透过它看到现在被剪切到其形状的后面淡色渐变 `background`。

Screenshot collage. Chrome (left) vs. Firefox (right), highlighting the differences in emoji shapes when they're part of knockout text. This is entirely normal and fine, as emojis look different depending on OS and browser.
剔除按钮文本(实时演示)。

好了,我们有了渐变文本,但现在缺少了渐变 `border`。所以我们将使用一个绝对定位的 `::before` 伪元素来添加它,该元素覆盖 `button` 的整个 `border-box` 区域,并从其父元素继承 `border`、`border-radius` 和 `background`(除了 `background-clip`,它被重置为 `border-box`)。

$b: .25em;

button {
  /* same as before */
  position: relative;
  border: solid $b transparent;
  
  &::before { 
    position: absolute;
    z-index: -1;
    inset: -$b;
    border: inherit;
    border-radius: inherit;
    background: inherit;
    background-clip: border-box;
    content: '';
  }
}

`inset: -$b` 是 一个简写,表示

top: -$b;
right: -$b;
bottom: -$b;
left: -$b

请注意,我们在这里使用带有负号的 `border-width` 值(`$b`)。`0` 值将使伪元素的 `margin-box`(在本例中与 `border-box` 相同,因为我们在 `::before` 上没有 `margin`)只覆盖其 `button` 父元素的 `padding-box`,而我们希望它覆盖整个 `border-box`。此外,正方向是向内的,但我们需要向外移动一个 `border-width` 才能从 `padding` 限制移动到 `border` 限制,因此使用负号——我们朝负方向移动。

我们还在这个绝对定位的元素上设置了一个负的 `z-index`,因为我们不希望它位于 `button` 文本之上,并阻止我们选择它。在这一点上,文本选择是区分文本和 `background` 的唯一方法,但我们很快就会解决这个问题!

Screenshot. Shows how text selection is the only way of still distinguishing the transparent text and gradient background clipped to text area button from its gradient background ::before pseudo that covers it fully.
添加渐变伪元素后的结果(实时演示)。

请注意,由于伪元素内容不可选择,因此选择只包含按钮的实际文本内容,而不包含 `::after` 伪元素中的表情符号。

下一步是添加一个两层 `mask`,它们之间有一个 `exclude` 合成操作,以便只留下这个伪元素的 `border` 区域可见。

button {
  /* same as before */
    
  &::before { 
    /* same as before */
    --full: linear-gradient(red 0 0);
    -webkit-mask: var(--full) padding-box, var(--full);
    -webkit-mask-composite: xor;
    mask: var(--full) padding-box exclude, var(--full);
  }
}

这几乎是我们之前方法中的一个中间阶段对实际 `button` 所做的操作。

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pill-shaped pastel gradient ghost button. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
使用额外的伪元素方法的最终结果(实时演示)。

我发现这在大多数情况下是最好的方法,当我们想要一个跨浏览器的解决方案,并且不包含 IE 或 pre-Chromium Edge 时,它们从未支持遮罩。

`border-image` 解决方案

在这一点上,你们中的一些人可能对着屏幕大喊大叫,说当我们可以使用渐变 `border-image` 来创建这种幽灵按钮时,没有必要使用 `::before` 伪元素——这是一个 已经工作了超过四分之三世纪的策略!

但是,使用 `border-image` 来创建药丸形按钮有一个很大的问题:这个属性与 `border-radius` 不兼容,这可以在下面的交互式演示中看到。一旦我们在具有 `border-radius` 的元素上设置了 `border-image`,我们就失去了 `border` 的圆角,即使有趣的是,`background` 仍然会尊重这种圆角。

不过,如果我们不需要圆角,或者所需的圆角最多为 `border-width` 的大小,这可能是一个简单的解决方案。

在不需要圆角的情况下,除了删除现在毫无意义的 `border-radius` 之外,我们不需要对初始样式进行太多更改

button {
  /* same base styles */
  --img: linear-gradient(to right bottom, var(--slist));
  border: solid .25em;
  border-image: var(--img) 1;
  background: var(--img) border-box;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

结果可以在下面看到,跨浏览器(应该即使在 pre-Chromium Edge 中也受支持)。

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pastel gradient ghost button with no rounded corners. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
使用 `border-image` 方法的没有圆角的结果(实时演示)。

所需的圆角小于 `border-width` 的技巧依赖于 `border-radius` 的工作方式。当我们设置这个属性时,我们设置的半径代表 `border-box` 角部的圆角。`padding-box` 角部的圆角(即 `border` 的内圆角)是 `border-radius` 减去 `border-width`(如果这个差值是正数)和 `0`(没有圆角)(否则)。这意味着如果 `border-radius` 小于或等于 `border-width`,则我们的 `border` 没有内圆角。

在这种情况下,我们可以使用 `inset()` 函数作为 `clip-path` 值,因为它也提供了对剪切矩形的角部进行圆角的可能性。如果你需要回顾一下这个函数的基础知识,你可以查看下面的插图

Illustration of how inset(d round r) works. Shows the clipping rectangle inside the element's border-box, its edges all a distance d away from the border limit. The corners of this clipping rectangle all have a rounding r along both axes.
`inset()` 函数的工作原理。

`inset()` 剪切掉元素 `border-box` 边缘距离之外的所有内容,指定方式与我们指定 `margin`、`border` 或 `padding` 的方式相同(使用一个、两个、三个或四个值)以及此矩形的圆角,指定方式与我们指定 `border-radius` 的方式相同(任何 有效的 `border-radius` 值 在这里也都有效)。

在我们的例子中,`border-box` 边缘的距离都是 `0`(我们不想从 `button` 的任何边缘剪切掉任何东西),但是我们有一个圆角,它必须最大为 `border-width`,这样没有内 `border` 圆角才有意义。

$b: .25em;

button {
  /* same as before */
  border: solid $b transparent;
  clip-path: inset(0 round $b)
}

请注意,`clip-path` 还会剪切掉我们可能在 `button` 元素上添加的任何外部阴影,无论它们是通过 `box-shadow` 还是 `filter: drop-shadow()` 添加的。

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pastel gradient ghost button with small rounded corners, the rounding radius being the same size as the border-width. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
使用 `border-image` 方法的小圆角结果(实时演示)。

虽然这种技术无法实现药丸形状的外观,但它确实具有如今拥有良好的支持的优势,并且在某些情况下可能正是我们需要的。

到目前为止,讨论的这三种解决方案可以在下面的演示中看到,该演示还包含一个 YouTube 链接,你可以从中看到我从头开始编写每种解决方案的代码,如果你更喜欢通过观看视频中的构建过程来学习,而不是阅读它们。

所有这些方法都在文本之外的 `padding-box` 中创建真正的透明度,因此它们适用于我们可能在 `button` 后面拥有的任何 `background`。但是,我们还有其他几种方法值得一提,即使它们在这方面存在限制。

覆盖解决方案

就像 `border-image` 方法一样,这是一种非常有限的策略。除非我们在 `button` 后面有一个纯色或固定的 `background`,否则它不起作用。

它涉及对具有不同 `background-clip` 值的背景进行分层,就像用于渐变边框的覆盖技术一样。唯一的区别是,在这里我们在模拟 `button` 元素后面 `background` 的渐变层之上添加一层渐变层,并将这层顶层剪切到 `text`。

$c: #393939;

html { background: $c; } 

button {
  /* same as before */
  --grad: linear-gradient(to right bottom, var(--slist));
  border: solid .25em transparent;
  border-radius: 9em;
  background: var(--grad) border-box, 
              linear-gradient($c 0 0) /* emulate bg behind button */, 
              var(--grad) border-box;
  -webkit-background-clip: text, padding-box, border-box;
  -webkit-text-fill-color: transparent;
}

不幸的是,这种方法在 Firefox 中无法正常工作,因为存在一个旧的 bug - 当文本设置为 `transparent` 时,不应用任何 `background-clip` 会导致按钮呈现出药丸形状,没有可见的文本。

Screenshot collage. Chrome (left) vs. Firefox (right). Chrome displays a pill-shaped pastel gradient ghost button. Firefox sadly only displays a pill-shaped button with no visible text.
所有 `background-clip` 覆盖解决方案(实时演示)。

我们仍然可以通过在 `::before` 伪元素上使用覆盖方法对渐变 `border` 进行处理,以及在实际的 `button` 上使用 `background-clip: text` 来实现跨浏览器兼容性。这基本上只是我们讨论的第二个解决方案的更有限的版本 - 我们仍然需要使用伪元素,但是由于我们使用覆盖而不是 `mask`,因此它只有在 `button` 后面有实色或固定 `background` 时才有效。

$b: .25em;
$c: #393939;

html { background: $c; } 

button {
  /* same base styles */
  --grad: linear-gradient(to right bottom, var(--slist));
  border: solid $b transparent;
  background: var(--grad) border-box;
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
  
  &::before {
    position: absolute;
    z-index: -1;
    inset: -$b;
    border: inherit;
    border-radius: inherit;
    background: linear-gradient($c 0 0) padding-box, 
                var(--grad) border-box;
    content: '';
  }
}

从好的方面来说,这个更有限的版本应该也能在 Chromium 之前的 Edge 中工作。

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pill-shaped pastel gradient ghost button that has a solid background behind. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
按钮后面有实色 `background` 的伪元素上的覆盖解决方案(实时演示)。

下面是固定 `background` 版本。

$f: url(balls.jpg) 50%/ cover fixed;

html { background: $f; } 

button {
  /* same as before */
  
  &::before {
    /* same as before */
    background: $f padding-box, 
                var(--grad) border-box
  }
}
Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pill-shaped pastel gradient ghost button that has a fixed image background behind. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
按钮后面有固定 `background` 的伪元素上的覆盖解决方案(实时演示)。

总的来说,我认为这并不是最好的策略,除非我们既符合 `background` 限制,又需要在不支持遮罩但支持将 `background` 剪切到 `text` 的浏览器(例如 Chromium 之前的 Edge)中复制这种效果。

混合解决方案

这种方法也是有限的,因为它只有在 `button` 下面的每个可见的渐变像素的通道值都大于或小于相应的 `background` 像素时才有效。但是,这并不是最糟糕的限制,因为它应该有助于提高页面对比度。

这里,我们首先将想要显示渐变的部分(即文本、图标和 `border`)设置为 `white` 或 `black`,具体取决于我们使用的是具有浅色渐变的深色主题还是具有深色渐变的浅色主题。`button` 的其余部分(文本和图标周围的区域,但在 `border` 内)与之前选择的 `color` 相反(如果我们将 `color` 值设置为 `black`,则为 `white`,否则为 `black`)。

在我们的例子中,我们在深色 `background` 上有一个非常浅色的渐变 `button`,因此我们从文本、图标和 `border` 的 `white` 开始,以及 `background` 的 `black`。我们两个渐变停点的十六进制通道值为 `ff` (`R`)、`da` (`G`)、`5f` (`B`) 和 `f9` (`R`)、`37` (`G`)、`6b` (`B`),因此对于任何通道值小于或等于 `min(ff, f9) = f9`(红色)、`min(da, 37) = 37`(绿色)和 `min(5f, 6b) = 5f`(蓝色)的 `background` 像素来说,都是安全的。

这意味着 `button` 后面的 `background-color` 应该具有小于或等于 `f9`、`37` 和 `5f` 的通道值,无论是作为实色 `background`,还是在使用 `multiply` 混合模式(总是生成至少与两个图层中较暗的图层一样暗的结果)混合的 `background-image` 图层下方。我们是在伪元素上设置这个 `background` 的,因为在 Chrome 中,与实际的 `body` 或 `html` 混合不起作用。

$b: .25em;

body::before {
  position: fixed;
  inset: 0;
  background: url(fog.jpg) 50%/ cover #f9375f;
  background-blend-mode: multiply;
  content: '';
}

button {
  /* same base styles */
  position: relative; /* so it shows on top of body::before */
  border: solid $b;
  background: #000;
  color: #fff;
  
  &::after {
    filter: brightness(0) invert(1);
    content: attr(data-ico);
  }
}

请注意,将图标完全设置为 `white` 意味着首先使用 `brightness(0)` 将其设置为 `black`,然后使用 `invert(1)` 反转这个 `black`。

Screenshot collage. Chrome (left) vs. Firefox (right). Both show a pill-shaped black and white (white border, white text, white emoji and black everything in between) button on top of a dark image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
`black` 和 `white` 按钮(实时演示)。

然后,我们添加一个渐变 `::before` 伪元素,就像我们在第一个跨浏览器方法中所做的那样。

button {
  /* same styles as before */
  position: relative;
  
  &::before {
    position: absolute;
    z-index: 2;
    inset: -$b;
    border-radius: inherit;
    background: linear-gradient(to right bottom, var(--slist);
    pointer-events: none;
    content: '';
  }
}

唯一的区别是,这里我们不是赋予它一个负的 `z-index`,而是赋予它一个正的 `z-index`。这样一来,它不仅位于实际的 `button` 上方,而且位于 `::after` 伪元素上方。我们还将 `pointer-events` 设置为 `none`,以便鼠标能够与下面实际的 `button` 内容交互。

Screenshot. Shows a pill-shaped gradient button with no visible text on top of a dark image background.
在 `black` 和 `white` 按钮上添加渐变伪元素后的结果(实时演示)。

现在,下一步是保留 `button` 的 `black` 部分,但用渐变替换 `white` 部分(即文本、图标和 `border`)。我们可以通过 `darken` 混合模式来实现,其中两个图层是带有 `::after` 图标的黑色和白色按钮,以及位于其顶部的渐变伪元素。

对于每个 RGB 通道,这种混合模式将采用两个图层的数值,并使用较暗(较小)的值作为结果。由于所有东西都比 `white` 更暗,因此结果图层在这个区域使用渐变像素值。由于 `black` 比所有东西都暗,因此结果图层在 `button` 为 `black` 的所有地方都是 `black`。

button {
  /* same styles as before */
  
  &::before {
    /* same styles as before */
    mix-blend-mode: darken;
  }
}
Screenshot collage.  Chrome (left) vs. Firefox (right). Both show a pill-shaped black and pastel gradient (pastel gradient border, text, emoji and black everything in between) button on top of a dark image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
“几乎完成”的结果(实时演示)。

好了,但只有在 `button` 后面的 `background` 是纯 `black` 的情况下,我们才能完成。否则,如果 `background` 的每个像素都比 `button` 上的对应渐变像素暗,我们可以应用第二个混合模式,这次是在实际的 `button` 上应用 `lighten`(之前,我们在 `::before` 伪元素上应用了 `darken`)。

对于每个 RGB 通道,这种混合模式将采用两个图层的数值,并使用较亮(较大)的值作为结果。由于任何东西都比 `black` 更亮,因此结果图层在 `button` 为 `black` 的所有地方使用 `button` 后面的 `background`。由于要求 `button` 的每个渐变像素都比其后面的 `background` 的对应像素亮,因此结果图层在这个区域使用渐变像素值。

button {
  /* same styles as before */
  mix-blend-mode: lighten;
}
Screenshot collage. Chrome (left) vs. Firefox (right). Both show a pill-shaped pastel gradient ghost with a 'BOO!' text and a ghost emoji button on top of a dark image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
深色背景上的浅色幽灵按钮(实时演示)。

对于浅色 `background` 上的深色渐变 `button`,我们需要切换混合模式。也就是说,在 `::before` 伪元素上使用 `lighten`,在 `button` 本身上使用 `darken`。首先,我们需要确保 `button` 后面的 `background` 足够亮。

假设我们的渐变在 `#602749` 和 `#b14623` 之间。我们渐变停点的通道值为 `60` (`R`)、`27` (`G`)、`49` (`B`) 和 `b1` (`R`)、`46` (`G`)、`23` (`R`),因此 `button` 后面的 `background` 需要具有至少 `max(60, b1) = b1`(红色)、`max(27, 46) = 46`(绿色)和 `max(49, 23) = 49`(蓝色)的通道值。

这意味着 `button` 上的 `background-color` 应该具有大于或等于 `b1`、`46` 和 `49` 的通道值,无论是作为实色 `background`,还是在 `background-image` 图层下方,使用 `screen` 混合模式(总是生成至少与两个图层中较亮的图层一样亮的結果)。

我们还需要将 `button` 的 `border`、文本和图标设置为 `black`,并将它的 `background` 设置为 `white`。

$b: .25em;

section {
  background: url(fog.jpg) 50%/ cover #b14649;
  background-blend-mode: screen;
}

button {
  /* same as before */
  border: solid $b;
  background: #fff;
  color: #000;
  mix-blend-mode: darken;

  &::before {
    /* same as before */
    mix-blend-mode: lighten
  }
  
  &::after {
    filter: brightness(0);
    content: attr(data-ico);
  }
}

在 `::after` 伪元素中的图标通过在它上面设置 `filter: brightness(0)` 变为 `black`。

Screenshot collage. Chrome (left) vs. Firefox (right). Both show a pill-shaped dark gradient ghost with a 'BOO!' text and a ghost emoji button on top of a light image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
浅色背景上的深色幽灵按钮(实时演示)。

我们还可以选择将所有 `button` 图层作为其 `background` 的一部分进行混合,无论是浅色主题还是深色主题,但是,正如前面提到的,Firefox 只是忽略了任何 `background-clip` 声明,其中 `text` 是值列表的一部分,而不是单个值。

好了,就是这样!我希望你正在(或者已经)度过一个快乐的万圣节。我的万圣节绝对是被我发现的(或重新发现的)所有 bug 搞得可怕的……以及现实情况是,它们至今还没有被修复。