最近,当我在寻找一些编码的想法时,因为我没有艺术感,所以我唯一能做的就是找到别人想出来的漂亮东西,然后用简洁的代码重新制作它们......我看到了这些糖果鬼魂按钮!它们似乎是我可以快速编码的最佳选择。
他们似乎是一个完美的选择,我可以快速地编码一个很酷的小东西。不到15分钟后,这就是我的Chromium成果。

纯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而没有实际接触到苹果设备的人(除非你算上四楼某人的iPhone 5--反正你也不想为这样的东西打扰他一个月两次以上--最近买的),我也学会了在将来使用Epiphany。感谢布赖恩的建议!
对于CSS部分,我们在一个::after 伪元素中添加图标,并在button 上使用grid 布局,以便为文本和图标提供良好的对齐方式。在button ,我们还设置了一个border ,一个padding ,一个border-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-origin 和background-clip 设置为border-box 。background-origin 既将background-position 的0 0 点放在它所设置的盒子的左上角,又给我们提供了background-size 所相对的盒子的尺寸。
0 0 0 0 50% 25% 50% padding-box 也就是说,如果background-origin 被设置为padding-box ,background-position 的点就在padding-box 的左上角。如果background-origin 被设置为border-box ,background-position 的点就在border-box 的左上角。如果background-origin 被设置为padding-box ,background-size 的意思是padding-box 的宽度和25% 的高度。border-box border-box 如果background-origin 被设置为border-box ,同样的background-size 的50% 25% 表示50% 的宽度和25% 的高度。
background-origin 的默认值是padding-box ,这意味着一个默认大小的100% 100% 梯度将覆盖padding-box ,然后在border 下面重复它(如果border 是完全不透明的,我们就看不到它)。然而,在我们的案例中,border 是完全的transparent ,我们希望我们的梯度能延伸到整个border-box 。这意味着我们需要将background-origin 的值改为border-box 。

简单,但不幸的是,非标准的Chromium解决方案
这涉及到使用三个mask 图层并将它们合成。如果你需要复习一下mask 的合成,你可以看看这个速成课程。
请注意,在CSSmask 图层的情况下,只有alpha通道是重要的,因为被遮盖元素的每个像素都会得到相应的mask 像素的alpha,而RGB通道不会以任何方式影响结果,所以它们可能是任何有效的值。下面,你可以看到purple 到transparent 梯度叠加的效果与使用完全相同的梯度作为mask 的效果。

我们要从最下面的两个层开始。第一个是完全不透明的层,完全覆盖整个border-box ,这意味着它的alpha值绝对是1 。0 另一个也是完全不透明的,但是(通过使用mask-clip )限制在padding-box ,这意味着,虽然这个层在整个padding-box ,它的alpha是1,但是在border ,它的alpha是transparent 。
如果你很难想象这一点,一个好的技巧是把一个元素的布局框想象成嵌套的矩形,就像下面说明的那样。

布局框(现场演示)。
在我们的例子中,底层是完全不透明的(alpha值为1 ),横跨整个橙色盒子(border-box )。第二层,我们放在第一层的上面,在整个红框(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 的限制下,差值是一个负值)。
CodePen嵌入回退
现在让我们回到我们的mask 图层,其中一个在整个border-box 上是完全不透明的,而它上面的一个在padding-box 区域是完全不透明的,在border 区域是完全透明的(在padding 限制和border 限制之间)。这两个图层使用exclude 操作进行合成(在非标准的 WebKit 版本中称为xor )。

两个基础层(现场演示)。
1 在两个图层的字母是0 或1 的情况下,这个操作的名字很有暗示性,就像在我们的例子中一样--第一个图层的字母在任何地方都是1 ,而第二个图层(我们放在第一个图层的上面)的字母在padding-box ,0 ,在padding 和border 之间的border 区域。
在这种情况下,非常直观的是布尔逻辑规则的应用--将两个相同的值进行XOR得到0 ,而将两个不同的值进行XOR得到1 。
在整个padding-box ,第一层和第二层的alpha值都是1 ,所以用这个操作将它们合成,在这个区域得到的层的alpha值是0 。然而,在border 区域(在padding 限制之外,但在border 限制之内),第一层的 alpha 值为1 ,而第二层的 alpha 值为0 ,因此我们在这个区域得到的结果层的 alpha 值为1 。
下面的交互式演示说明了这一点,你可以在查看两个mask ,在3D中分离的层和使用此操作查看它们的堆叠和合成之间切换。
CodePen嵌入回退
把东西放到代码中,我们有:
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和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 ,有真正的透明度。

遮住整个padding-box (现场演示)后的结果。
这不是我们想要的结果。虽然我们有一个漂亮的渐变border (顺便说一下,这是我喜欢的获得渐变border 的方法,因为我们有真正的透明的整个padding-box ,而不仅仅是一个封面),但我们现在缺少文本。
因此,下一步是在之前的图层上再加一个mask ,这次是限制在text (同时也使实际的文本完全transparent ,这样我们就可以通过它看到梯度background ),并将这第三个mask 图层与前两个图层XOR的结果(结果可以在上面的截图中看到)。
下面的互动演示允许查看三个mask ,既可以在3D中分离,也可以堆叠和合成。
CodePen嵌入回退
请注意,text 的值 [mask-clip](https://css-tricks.com/almanac/properties/m/mask-clip/)的值是非标准的,所以,很遗憾,这只在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;
}
}

我真的很喜欢这种方法,这是我们目前最简单的方法,我真的希望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 ,现在这个渐变被剪切到了它的形状。

好了,我们有了梯度文本,但现在我们缺少梯度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 的唯一方法,但我们很快就会解决这个问题

注意,由于伪元素的内容是不可选择的,选择只包括按钮的实际文本内容,而不包括::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 :

我发现在大多数情况下,当我们想要一些跨浏览器的东西时,这是最好的方法,这不包括IE或前Chromium Edge,它们都不支持屏蔽。
border-image 的解决方案
在这一点上,你们中的一些人可能会对着屏幕大叫,说没有必要使用::before 伪元素,因为我们可以使用梯度border-image 来创建这种幽灵按钮--这是一种已经工作了超过四分之三年的战术
然而,使用border-image 来制作药丸状的按钮有一个非常大的问题:这个属性与border-radius 不合拍,在下面的互动演示中可以看到。只要我们在一个带有border-radius 的元素上设置一个border-image ,我们就会失去border 的圆角,即使如此,有趣的是,background 仍然会尊重这个圆角。
CodePen嵌入回退
尽管如此,这可能是一个简单的解决方案,在不需要圆角的情况下,或者所需的圆角最多是 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;
}
结果可以在下面看到,跨浏览器(即使在Chromium Edge之前也应该支持)
使用border-image 方法的无圆角结果(现场演示)。
所需的圆角比border-width ,这个技巧依赖于border-radius 的工作方式。当我们设置这个属性时,我们设置的半径代表了border-box 的角部圆整。padding-box 的角部圆整(也就是border 的内部圆整)是border-radius 减去border-width ,如果这个差值是正的,0 (无圆整)。这意味着如果border-radius 小于或等于border-width ,我们就没有对border 的内部舍入。
在这种情况下,我们可以使用inset() 函数作为clip-path 值,因为它也提供了对剪裁矩形的角进行舍入的可能性。如果你需要复习一下这个函数的基本知识,你可以看看下面的插图。

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() 添加的。

使用border-image 方法的小角圆角结果(现场演示)。
虽然这种技术不能实现药丸形状的外观,但它的优点是现在有很大的支持,在某些情况下,它可能是我们所需要的全部。
到目前为止讨论的三种解决方案可以在下面的演示中看到,其中也有一个YouTube链接,如果你喜欢通过观看视频构建的东西而不是阅读它们来学习,你可以看到我从头开始编码。
CodePen嵌入回退
button所有这些方法都在文本之外的padding-box ,创建真正的透明度,因此它们适用于我们可能在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中由于一个旧的错误而失败了--只是没有应用任何background-clip ,同时也使文本transparent ,产生一个没有可见文本的药丸状按钮。

所有background-clip 盖的解决方案(现场演示)。
我们仍然可以通过在::before 伪基站和background-clip: text 实际button 上使用渐变border 的覆盖方法来实现跨浏览器,这基本上只是我们讨论的第二个解决方案的一个更有限的版本--我们仍然需要使用一个伪基站,但是,由于我们使用一个覆盖,而不是一个mask ,它只在我们有一个固体或固定background 在button 。
$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中工作。

在按钮后面有一个实心的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
}
}

在按钮后面有一个固定的background (现场演示)的伪装方案。
总的来说,我不认为这是最好的策略,除非我们都符合background 的限制,而且我们需要在不支持遮盖,但支持将background 剪切到text 的浏览器中重现效果,例如Chromium Edge之前的浏览器。
融合解决方案
button这个方法是另一个有限的方法,因为它不会工作,除非对于每一个可见的渐变像素,其通道的值都比background 下方的相应像素大或小。然而,这并不是最糟糕的限制,因为它可能会使我们的页面有更好的对比。
在这里,我们先把我们想要有渐变的部分(即文字、图标和border )做成white 或black ,这取决于我们是用浅色渐变的暗主题还是用深色渐变的亮主题。button 的其余部分(文本和图标周围的区域,但在border 内)是之前选择的color 的逆值(如果我们将color 的值设置为black ,则white ,否则black )。
在我们的例子中,我们有一个相当浅的渐变button ,在一个深色的background ,所以我们开始用white ,用于文字、图标和边界,black ,用于background 。我们的两个渐变站的十六进制通道值是ff (R),da (G),5f (B) 和f9 (R),37 (G),6b (B), 所以我们使用任何background 像素都是安全的,其通道值最多和红色的min(ff, f9) = f9, 绿色的min(da, 37) = 37 和蓝色的min(5f, 6b) = 5f 一样大。
这意味着在我们的button 后面有一个通道值小于或等于f9,37 和5f 的background-color ,或者作为一个实心的background ,或者在我们使用multiply 混合模式混合的background-image 图层下面(这总是产生一个至少与两层中较暗的一层一样暗的结果)。我们在一个伪元素上设置这个background ,因为与实际的body 或html 混合在Chrome中不起作用。
$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);
}
}
black 请注意,使图标完全white ,意味着首先用brightness(0) ,然后用invert(1) ,将这个black 。

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 内容互动。

在black 和white 按钮上添加渐变伪数后的结果(现场演示)。
button现在,下一步是保留我们的black 部分,但用梯度替换white 部分(即文字、图标和border )。我们可以用darken 混合模式来做到这一点,其中的两个图层是带有::after 图标的黑白按钮和它上面的渐变假象。
对于RGB通道中的每一个,这个混合模式采取两个层的值,并使用较暗(较小)的那一个作为结果。因为所有东西都比white ,所以结果层使用该区域的梯度像素值。由于black 比所有东西都要暗,所以结果层是black ,到处都是button 是black 。
button {
/* same styles as before */
&::before {
/* same styles as before */
mix-blend-mode: darken;
}
}

好吧,但我们只有在button 后面的background 是纯粹的black ,才会在这一点上完成。button 否则,如果一个background ,其每个像素都比我们的button 上的梯度的相应像素更暗,我们可以应用第二个混合模式,这次是在实际的lighten (之前,我们在::before 伪装上有darken )。
对于RGB通道中的每一个,这个混合模式采用两个图层的值,并使用较轻(较大)的那个作为结果。因为任何东西都比black 轻,所以结果层在button 后面使用background ,而在button 后面使用black 。而且由于一个要求是button 的每一个渐变像素都比它后面的background 的相应像素要浅,所以结果层使用该区域的渐变像素值。
button {
/* same styles as before */
mix-blend-mode: lighten;
}

对于在浅色background 上的深色渐变button ,我们需要切换混合模式。也就是说,在::before 伪装上使用lighten ,在button 本身上使用darken 。首先,我们需要确保button 后面的background 是足够亮的。
假设我们的梯度在#602749 和#b14623 之间。background 我们的梯度站的通道值是60 (R),27 (G),49 (B) 和b1 (R),46 (G),23 (R), 所以button 后面的通道值需要至少是max(60, b1) = b1 的红色,max(27, 46) = 46 的绿色和max(49, 23) = 49 的蓝色。
这意味着在我们的button 上有一个通道值大于或等于b1,46 和49 的background-color ,或者作为一个实心的background ,或者在一个background-image 图层下面,使用screen 混合模式(这总是产生一个至少与两个图层中较轻的那个一样的结果)。
我们还需要使button 的边框、文本和图标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 :

我们还可以选择将所有的button 层作为其background 的一部分进行混合,无论是浅色还是深色主题,但是,如前所述,Firefox只是忽略了任何background-clip 声明,其中text 是一个值列表的一部分,而不是单一的值。
好了,就这样吧!我希望你有(或有)一个可怕的万圣节。我的万圣节肯定是被我发现的所有bug......或重新发现的bug,以及它们还没有被修复的现实所吓倒的。