Kürzlich, als ich nach Ideen für mein nächstes Coding-Projekt suchte, da ich keinen künstlerischen Sinn habe und daher nur schöne Dinge nachbauen kann, die andere Leute entworfen haben, mit sauberem und kompaktem Code… stieß ich auf diese Candy Ghost Buttons!
Sie schienen die perfekte Wahl für eine coole kleine Sache zu sein, die ich schnell codieren konnte. Weniger als fünfzehn Minuten später war dies mein Chromium-Ergebnis

Ich dachte, die Technik sei es wert, geteilt zu werden. Daher werden wir in diesem Artikel durchgehen, wie ich es zuerst gemacht habe und welche anderen Optionen wir haben.
Der Ausgangspunkt
Ein Button wird erstellt mit… sind Sie bereit dafür? Ein button-Element! Dieses button-Element hat ein data-ico-Attribut, in das wir ein Emoji einfügen. Es hat auch eine benutzerdefinierte Stopplisten-Eigenschaft, --slist, die im style-Attribut gesetzt ist.
<button data-ico="👻" style="--slist: #ffda5f, #f9376b">boo!</button>
Nachdem ich den Artikel geschrieben hatte, erfuhr ich, dass Safari eine Reihe von Problemen mit dem Clipping auf text hat, nämlich funktioniert es nicht auf button-Elementen oder auf Elementen mit display: flex (und vielleicht auch auf grid?), ganz zu schweigen vom Text der Kindelemente. Leider bedeutet dies, dass alle hier vorgestellten Techniken in Safari fehlschlagen. Die einzige Problemumgehung besteht darin, alle button-Stile von hier auf ein span-Element anzuwenden, das in den button verschachtelt ist und den border-box seines Elternteils abdeckt. Und falls dies jemandem hilft, der, wie ich, auf Linux lebt und keinen physischen Zugang zu einem Apple-Gerät hat (es sei denn, man zählt das iPhone 5, das jemand im vierten Stock kürzlich gekauft hat – mit dem man sowieso nicht öfter als zweimal im Monat mit solchen Dingen behelligen möchte), habe ich auch gelernt, zukünftig Epiphany zu verwenden. Danke an Brian für den Vorschlag!
Für den CSS-Teil fügen wir das Icon in ein ::after-Pseudoelement ein und verwenden ein grid-Layout für den button, um eine schöne Ausrichtung von Text und Icon zu erzielen. Auf dem button setzen wir auch einen border, ein padding, einen border-radius, verwenden die Stoppliste --slist für einen diagonalen Verlauf und verschönern die 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) }
}
Es gibt eine Sache, die im obigen Code klargestellt werden muss. In der hervorgehobenen Zeile setzen wir sowohl background-origin als auch background-clip auf border-box. background-origin platziert den 0 0-Punkt für background-position in der oberen linken Ecke der Box, auf die es gesetzt ist, und gibt uns die Box, zu der die Abmessungen von background-size relativ sind.
Das heißt, wenn background-origin auf padding-box gesetzt ist, befindet sich der 0 0-Punkt für background-position in der oberen linken Ecke der padding-box. Wenn background-origin auf border-box gesetzt ist, befindet sich der 0 0-Punkt für background-position in der oberen linken Ecke der border-box. Wenn background-origin auf padding-box gesetzt ist, bedeutet eine background-size von 50% 25% 50% der Breite der padding-box und 25% der Höhe der padding-box. Wenn background-origin auf border-box gesetzt ist, bedeutet die gleiche background-size von 50% 25% 50% der Breite der border-box und 25% der Höhe der border-box.
Der Standardwert für background-origin ist padding-box, was bedeutet, dass ein standardmäßig großer 100% 100%-Verlauf die padding-box abdeckt und sich dann unter dem border wiederholt (wo wir ihn nicht sehen können, wenn der border vollständig opak ist). In unserem Fall ist der border jedoch vollständig transparent und wir möchten, dass unser Verlauf die gesamte border-box ausfüllt. Das bedeutet, wir müssen den Wert von background-origin auf border-box ändern.

Die einfache, aber leider nicht standardmäßige Chromium-Lösung
Dies beinhaltet die Verwendung von drei mask-Ebenen und deren Komposition. Wenn Sie eine Auffrischung zur mask-Komposition benötigen, können Sie sich diesen Crashkurs ansehen.
Beachten Sie, dass bei CSS-mask-Ebenen *nur der Alphakanal zählt*, da jedes Pixel des maskierten Elements die Alpha des entsprechenden mask-Pixels erhält, während die RGB-Kanäle das Ergebnis in keiner Weise beeinflussen, sodass sie jeden gültigen Wert haben können. Unten sehen Sie die Wirkung eines purple-zu-transparent-Gradient-Overlays im Vergleich zur Wirkung der Verwendung desselben Gradienten als mask.

Wir beginnen mit den unteren beiden Ebenen. Die erste ist eine vollständig opake Ebene, die die gesamte border-box vollständig abdeckt, was bedeutet, dass sie absolut überall eine Alpha von 1 hat. Die andere ist ebenfalls vollständig opak, aber (durch Verwendung von mask-clip) auf die padding-box beschränkt, was bedeutet, dass diese Ebene zwar überall in der padding-box eine Alpha von 1 hat, im border-Bereich jedoch vollständig transparent ist und dort eine Alpha von 0 hat.
Wenn Sie sich das schwer vorstellen können, ist ein guter Trick, sich die Layout-Boxen eines Elements als verschachtelte Rechtecke vorzustellen, so wie sie unten dargestellt sind.

In unserem Fall ist die untere Ebene über die gesamte orangefarbene Box (die border-box) vollständig opak (der Alpha-Wert ist 1). Die zweite Ebene, die wir über die erste legen, ist über die gesamte rote Box (die padding-box) vollständig opak (der Alpha-Wert ist 1) und im Bereich zwischen der padding-Grenze und der border-Grenze vollständig transparent (mit einer Alpha von 0).
Eine wirklich coole Sache an den Grenzen dieser Boxen ist, dass die Eckabrundung durch den border-radius bestimmt wird (und im Fall der padding-box auch durch die border-width). Dies wird durch die unten interaktive Demo veranschaulicht, wo wir sehen können, wie die Eckabrundung der border-box durch den border-radius-Wert gegeben ist, während die Eckabrundung der padding-box als border-radius abzüglich border-width berechnet wird (begrenzt auf 0, falls die Differenz negativ ist).
Kommen wir nun zu unseren mask-Ebenen zurück: eine ist über die gesamte border-box vollständig opak, während die darüber liegende Ebene über die padding-box vollständig opak und im border-Bereich (zwischen der padding-Grenze und der border-Grenze) vollständig transparent ist. Diese beiden Ebenen werden mit der exclude-Operation (in der nicht standardmäßigen WebKit-Version als xor bezeichnet) komponiert.

Der Name dieser Operation ist in der Situation, in der die Alphas der beiden Ebenen entweder 0 oder 1 sind, wie in unserem Fall – die Alpha der ersten Ebene ist überall 1, während die Alpha der zweiten Ebene (die wir darüber legen) innerhalb der padding-box 1 und im border-Bereich zwischen der padding-Grenze und der border-Grenze 0 ist.
In dieser Situation ist es ziemlich intuitiv, dass die Regeln der Booleschen Logik gelten – XOR von zwei identischen Werten ergibt 0, während XOR von zwei unterschiedlichen Werten 1 ergibt.
Überall in der padding-box haben sowohl die erste als auch die zweite Ebene eine Alpha von 1, daher ergibt deren Komposition mit dieser Operation eine Alpha von 0 für die resultierende Ebene in diesem Bereich. Im border-Bereich (außerhalb der padding-Grenze, aber innerhalb der border-Grenze) hat die erste Ebene eine Alpha von 1, während die zweite eine Alpha von 0 hat, so dass wir in diesem Bereich eine Alpha von 1 für die resultierende Ebene erhalten.
Dies wird durch die interaktive Demo unten veranschaulicht, in der Sie zwischen der getrennten Ansicht der beiden mask-Ebenen in 3D und deren gestapelter und kompositierter Ansicht umschalten können.
Umgesetzt in Code haben wir
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);
}
Bevor wir weitermachen, besprechen wir einige Details zur Feinabstimmung des obigen CSS.
Erstens, da die vollständig opak-Ebenen alles sein können (die Alpha-Kanäle sind fest, immer 1 und die RGB-Kanäle spielen keine Rolle), mache ich sie normalerweise red – nur drei Zeichen! In diesem Sinne würde die Verwendung eines konischen Gradienten anstelle eines linearen auch ein Zeichen sparen, aber das mache ich selten, da wir immer noch mobile Browser haben, die Maskierung unterstützen, aber keine konischen Gradienten unterstützen. Die Verwendung eines linearen Gradienten stellt sicher, dass wir eine breite Unterstützung haben. Nun, abgesehen von IE und dem Vor-Chromium-Edge, aber da funktioniert sowieso kaum etwas cooles und schickes.
Zweitens verwenden wir für beide Ebenen Gradienten. Wir verwenden keine einfache background-color für die untere Ebene, da wir für die background-color selbst keine separate background-clip-Einstellung vornehmen können. Wenn wir die background-image-Ebene auf die padding-box zuschneiden würden, würde dieser background-clip-Wert auch für die darunter liegende background-color gelten – sie würde ebenfalls auf die padding-box zugeschnitten werden und wir hätten keine Möglichkeit, sie die gesamte border-box abdecken zu lassen.
Drittens setzen wir keinen expliziten mask-clip-Wert für die untere Ebene, da der Standardwert für diese Eigenschaft genau der Wert ist, den wir in diesem Fall wollen: border-box.
Viertens können wir die standardmäßige mask-composite (unterstützt von Firefox) in die mask-Kurzschreibweise aufnehmen, aber nicht die nicht standardmäßige (unterstützt von WebKit-Browsern).
Und schließlich setzen wir immer die Standardversion zuletzt, damit sie jede nicht standardmäßige Version überschreibt, die möglicherweise ebenfalls unterstützt wird.
Das Ergebnis unseres bisherigen Codes (noch Cross-Browser) sieht wie unten aus. Wir haben auch ein background-image auf dem Root-Element hinzugefügt, damit offensichtlich ist, dass wir echte Transparenz im padding-box-Bereich haben.

padding-box (Live-Demo).Das ist nicht, was wir wollen. Obwohl wir einen schönen Farbverlauf-border haben (und übrigens ist dies meine bevorzugte Methode, um einen Farbverlauf-border zu erhalten, da wir echte Transparenz im gesamten padding-box-Bereich haben und nicht nur eine Abdeckung), fehlt uns jetzt der Text.
Der nächste Schritt ist also, den Text mit einer weiteren mask-Ebene darüber wieder hinzuzufügen, diesmal eine, die auf text beschränkt ist (während wir gleichzeitig den eigentlichen Text vollständig transparent machen, damit wir den Farbverlauf-background durch ihn hindurch sehen können) und diese dritte mask-Ebene mit dem Ergebnis der XOR-Verbindung der ersten beiden (das Ergebnis, das im obigen Screenshot zu sehen ist) XOR-verbinden.
Die interaktive Demo unten ermöglicht die Ansicht der drei mask-Ebenen sowohl getrennt in 3D als auch gestapelt und komposit.
Beachten Sie, dass der Wert text für mask-clip nicht standardmäßig ist, also funktioniert das leider nur in Chrome. In Firefox erhalten wir keine Maskierung des Buttons mehr, und da wir den Text transparent gemacht haben, erhalten wir nicht einmal eine gute Abwärtskompatibilität.
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);
}
Wenn wir unseren button auf diese Weise nicht unbrauchbar machen wollen, sollten wir den Code, der die mask anwendet und den Text transparent macht, in einen @supports-Block setzen.
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;
}
}

Ich mag diese Methode wirklich, sie ist die einfachste, die wir bisher haben, und ich wünschte wirklich, text wäre ein Standardwert für mask-clip und alle Browser würden ihn richtig unterstützen.
Wir haben jedoch auch einige andere Methoden, um den Candy-Ghost-Button-Effekt zu erzielen, und obwohl sie entweder komplizierter oder eingeschränkter sind als die nicht standardmäßige Chromium-only-Lösung, die wir gerade besprochen haben, werden sie auch besser unterstützt. Lassen Sie uns diese also betrachten.
Die Lösung mit zusätzlichem Pseudoelement
Dies beinhaltet das Setzen der gleichen anfänglichen Stile wie zuvor, aber anstatt einer mask schneiden wir den background auf den text-Bereich zu.
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
}
Genau wie zuvor müssen wir auch den tatsächlichen Text transparent machen, damit wir durch ihn den Pastellfarben-Gradient-background dahinter sehen können, der nun seiner Form angepasst ist.

Okay, wir haben den Farbverlauf-Text, aber jetzt fehlt uns der Farbverlauf-border. Also werden wir ihn mit einem absolut positionierten ::before-Pseudoelement hinzufügen, das den gesamten border-box-Bereich des button abdeckt und den border, border-radius und background von seinem Elternteil erbt (abgesehen von background-clip, das auf border-box zurückgesetzt wird).
$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 ist eine Kurzschreibweise für
top: -$b;
right: -$b;
bottom: -$b;
left: -$b
Beachten Sie, dass wir hier den Wert von border-width ($b) mit einem Minuszeichen verwenden. Der Wert 0 würde die margin-box des Pseudoelements (in diesem Fall identisch mit der border-box, da wir keine margin im ::before haben) nur die padding-box seines button-Elternteils abdecken lassen, und wir möchten, dass es die gesamte border-box abdeckt. Außerdem ist die positive Richtung nach innen, aber wir müssen nach außen um eine border-width gehen, um von der padding-Grenze zur border-Grenze zu gelangen, daher das Minuszeichen – wir gehen in die negative Richtung.
Wir haben auch einen negativen z-index auf diesem absolut positionierten Element gesetzt, da wir nicht wollen, dass es über dem button-Text liegt und uns daran hindert, ihn auszuwählen. Zu diesem Zeitpunkt ist die Textauswahl die einzige Möglichkeit, den Text vom background zu unterscheiden, aber das werden wir bald beheben!

Beachten Sie, dass da Pseudoelement-Inhalte nicht auswählbar sind, umfasst die Auswahl nur den tatsächlichen Textinhalt des Buttons und nicht auch das Emoji im ::after-Pseudoelement.
Der nächste Schritt ist das Hinzufügen einer zweistufigen mask mit einer exclude-Kompositionsoperation zwischen ihnen, um nur den border-Bereich dieses Pseudoelements sichtbar zu lassen.
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);
}
}
Dies ist ziemlich genau das, was wir für den tatsächlichen button in einer der Zwischenstufen der vorherigen Methode gemacht haben.

Ich finde dies in den meisten Fällen den besten Ansatz, wenn wir etwas Cross-Browser-kompatibles wollen, das IE oder Pre-Chromium Edge nicht einschließt, da diese nie Maskierung unterstützt haben.
Die border-image-Lösung
An diesem Punkt schreien vielleicht einige von Ihnen, dass es nicht nötig ist, das ::before-Pseudoelement zu verwenden, wenn wir ein Farbverlauf-border-image verwenden könnten, um diese Art von Ghost-Button zu erstellen – eine Taktik, die seit über dreieinhalb Jahrzehnten funktioniert!
Es gibt jedoch ein sehr großes Problem bei der Verwendung von border-image für pillenförmige Buttons: Diese Eigenschaft verträgt sich nicht gut mit border-radius, wie in der interaktiven Demo unten zu sehen ist. Sobald wir ein border-image auf ein Element mit border-radius setzen, verlieren wir die Eckabrundung des border, auch wenn der background diese Rundung lustigerweise weiterhin respektiert.
Dennoch kann dies eine einfache Lösung sein, wenn keine Eckabrundung benötigt wird oder die gewünschte Eckabrundung *höchstens* die Größe der border-width hat.
Im Fall ohne Eckabrundung, abgesehen vom Wegfall des nun sinnlosen border-radius, müssen wir die anfänglichen Stile nicht stark ändern
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;
}
Das Ergebnis ist unten zu sehen, Cross-Browser (sollte auch im Pre-Chromium-Edge unterstützt werden).

border-image-Methode (Live-Demo).Der Trick mit der gewünschten Eckabrundung, die kleiner als die border-width ist, beruht auf der Funktionsweise von border-radius. Wenn wir diese Eigenschaft setzen, repräsentiert der gesetzte Radius die Abrundung für die Ecken der border-box. Die Abrundung für die Ecken der padding-box (die innere Abrundung des border) ist der border-radius abzüglich der border-width, wenn diese Differenz positiv ist, und 0 (keine Abrundung) andernfalls. Das bedeutet, wir haben keine innere Abrundung für den border, wenn der border-radius kleiner oder gleich der border-width ist.
In dieser Situation können wir die inset()-Funktion als Wert für clip-path verwenden, da sie auch die Möglichkeit bietet, die Ecken des Clipping-Rechtecks abzurunden. Wenn Sie eine Auffrischung zu den Grundlagen dieser Funktion benötigen, können Sie sich die folgende Illustration ansehen

inset()-Funktion funktioniert.inset() schneidet alles außerhalb eines Clipping-Rechtecks aus, das durch die Abstände zu den Kanten der border-box des Elements definiert ist, spezifiziert auf dieselbe Weise, wie wir margin, border oder padding angeben würden (mit einem, zwei, drei oder vier Werten) und die Eckabrundung für dieses Rechteck, spezifiziert auf dieselbe Weise, wie wir border-radius angeben würden (jeder gültige border-radius-Wert ist auch hier gültig).
In unserem Fall sind die Abstände zu den Rändern der border-box alle 0 (wir wollen nichts von den Rändern des button abschneiden), aber wir haben eine Abrundung, die höchstens so groß wie die border-width sein darf, damit das Fehlen einer inneren border-Abrundung sinnvoll ist.
$b: .25em;
button {
/* same as before */
border: solid $b transparent;
clip-path: inset(0 round $b)
}
Beachten Sie, dass clip-path auch alle äußeren Schatten, die wir dem button-Element hinzufügen, abschneiden wird, egal ob sie über box-shadow oder filter: drop-shadow() hinzugefügt werden.

border-image-Methode (Live-Demo).Während diese Technik nicht die Pillenform erzielen kann, hat sie den Vorteil einer großartigen Unterstützung heutzutage und kann in bestimmten Situationen alles sein, was wir brauchen.
Die bisher besprochenen drei Lösungen können in der untenstehenden Demo zusammengestellt werden, die auch einen YouTube-Link enthält, wo Sie mich dabei beobachten können, wie ich jede davon von Grund auf neu codiere, wenn Sie es vorziehen, durch das Ansehen von Dingen, die im Video erstellt werden, zu lernen, anstatt darüber zu lesen.
Alle diese Methoden erzeugen echte Transparenz in der padding-box außerhalb des Textes, sodass sie für jeden background funktionieren, den wir hinter dem button haben könnten. Wir haben jedoch noch ein paar weitere Methoden, die erwähnenswert sein könnten, auch wenn sie in dieser Hinsicht Einschränkungen haben.
Die Cover-Lösung
Ähnlich wie der Ansatz mit border-image ist dies eine ziemlich eingeschränkte Taktik. Sie funktioniert nicht, es sei denn, wir haben einen soliden oder festen background hinter dem button.
Es beinhaltet das Schichten von Hintergründen mit unterschiedlichen background-clip-Werten, ähnlich der Cover-Technik für Farbverlauf-Ränder. Der einzige Unterschied ist, dass wir hier eine weitere Farbverlauf-Ebene über diejenige legen, die den background hinter unserem button-Element emuliert, und diese oberste Ebene auf text zuschneiden.
$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;
}
Leider schlägt dieser Ansatz in Firefox aufgrund eines alten Bugs fehl – das bloße Nicht-Anwenden von background-clip, während der Text gleichzeitig transparent gemacht wird, erzeugt einen pillenförmigen Button ohne sichtbaren Text.

background-clip-Cover-Lösung (Live-Demo).Wir könnten es immer noch Cross-Browser-kompatibel machen, indem wir die Cover-Methode für den Farbverlauf-border auf einem ::before-Pseudoelement und background-clip: text auf dem eigentlichen button verwenden, was im Grunde nur eine eingeschränktere Version der zweiten besprochenen Lösung ist – wir müssen immer noch ein Pseudoelement verwenden, aber da wir ein Cover und keine mask verwenden, funktioniert es nur, wenn wir einen soliden oder festen background hinter dem button haben.
$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: '';
}
}
Auf der positiven Seite sollte diese eingeschränktere Version auch im Pre-Chromium-Edge funktionieren.

background hinter dem Button (Live-Demo).Unten sehen Sie auch die feste background-Version.
$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 hinter dem Button (Live-Demo).Insgesamt glaube ich nicht, dass dies die beste Taktik ist, es sei denn, wir passen sowohl in die background-Beschränkung als auch müssen wir den Effekt in Browsern reproduzieren, die keine Maskierung unterstützen, aber das Zuschneiden des background auf den text unterstützen, wie z. B. Pre-Chromium Edge.
Die Blending-Lösung
Dieser Ansatz ist ebenfalls eingeschränkt, da er nicht funktioniert, es sei denn, für jedes einzelne Gradientenpixel, das sichtbar ist, haben seine Kanäle Werte, die entweder alle größer oder alle kleiner sind als das entsprechende Pixel des background unter dem button. Dies ist jedoch nicht die schlimmste Einschränkung, da sie wahrscheinlich zu einer besseren Kontrastierung unserer Seite führen sollte.
Hier beginnen wir damit, die Teile, an denen wir den Farbverlauf haben wollen (d. h. den Text, das Icon und den border), entweder white oder black zu machen, je nachdem, ob wir ein dunkles Thema mit einem hellen Farbverlauf oder ein helles Thema mit einem dunklen Farbverlauf haben. Der Rest des button (der Bereich um den Text und das Icon, aber innerhalb des border) ist das Gegenteil der zuvor gewählten color (white, wenn wir den color-Wert auf black setzen, und umgekehrt).
In unserem Fall haben wir einen ziemlich hellen Farbverlauf-button auf einem dunklen background, also beginnen wir mit white für Text, Icon und Border und black für den background. Die Hex-Kanalwerte unserer beiden Farbverlauf-Stopps sind ff (R), da (G), 5f (B) und f9 (R), 37 (G), 6b (B), sodass wir bei allen background-Pixeln mit Kanalwerten, die höchstens min(ff, f9) = f9 für Rot, min(da, 37) = 37 für Grün und min(5f, 6b) = 5f für Blau sind, auf der sicheren Seite wären.
Das bedeutet, dass wir einen background-color hinter unserem button mit Kanalwerten haben, die kleiner oder gleich f9, 37 und 5f sind, entweder als eigenständiger fester background oder unter einer background-image-Ebene, die wir mit dem multiply-Blendmodus überblenden (der immer ein Ergebnis erzeugt, das mindestens so dunkel ist wie die dunklere der beiden Ebenen). Wir setzen diesen background auf ein Pseudoelement, da das Überblenden mit dem tatsächlichen body oder html in Chrome nicht funktioniert.
$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);
}
}
Beachten Sie, dass die vollständige white-Farbe des Icons bedeutet, dass es zuerst black mit brightness(0) und dann dieses black mit invert(1) invertiert wird.

Wir fügen dann ein Farbverlauf-::before-Pseudoelement hinzu, genau wie wir es für die erste Cross-Browser-Methode getan haben.
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: '';
}
}
Der einzige Unterschied ist, dass wir ihm hier anstelle eines negativen z-index einen positiven z-index geben. So liegt es nicht nur über dem eigentlichen button, sondern auch über dem ::after-Pseudoelement, und wir setzen pointer-events auf none, um der Maus die Interaktion mit dem eigentlichen button-Inhalt darunter zu ermöglichen.

Nun besteht der nächste Schritt darin, die black-Teile unseres button zu behalten, aber die white-Teile (d. h. Text, Icon und border) durch den Farbverlauf zu ersetzen. Dies können wir mit einem darken-Blendmodus erreichen, bei dem die beiden Ebenen der schwarz-weiße Button mit dem ::after-Icon und dem Farbverlauf-Pseudoelement darüber sind.
Für jeden der RGB-Kanäle nimmt dieser Blendmodus die Werte der beiden Ebenen und verwendet den dunkleren (kleineren) für das Ergebnis. Da alles dunkler als white ist, verwendet die resultierende Ebene die Farbverlauf-Pixelwerte in diesem Bereich. Da black dunkler ist als alles andere, ist die resultierende Ebene überall dort black, wo der button black ist.
button {
/* same styles as before */
&::before {
/* same styles as before */
mix-blend-mode: darken;
}
}

Okay, aber wir wären an diesem Punkt nur fertig, wenn der background hinter dem button reines black wäre. Andernfalls, im Fall eines background, dessen jedes Pixel dunkler ist als das entsprechende Pixel des Farbverlaufs auf unserem button, können wir einen zweiten Blendmodus anwenden, diesmal lighten auf dem eigentlichen button (zuvor hatten wir darken auf dem ::before-Pseudoelement).
Für jeden der RGB-Kanäle nimmt dieser Blendmodus die Werte der beiden Ebenen und verwendet den helleren (größeren) für das Ergebnis. Da alles heller als black ist, verwendet die resultierende Ebene überall dort den background hinter dem button, wo der button black ist. Und da eine Anforderung ist, dass jedes Farbverlauf-Pixel des button heller ist als das entsprechende Pixel des dahinter liegenden background, verwendet die resultierende Ebene die Farbverlauf-Pixelwerte in diesem Bereich.
button {
/* same styles as before */
mix-blend-mode: lighten;
}

Für einen dunklen Farbverlauf-button auf einem hellen background müssen wir die Blendmodi umstellen. Das heißt, lighten auf dem ::before-Pseudoelement und darken auf dem button selbst. Und zuerst müssen wir sicherstellen, dass der background hinter dem button hell genug ist.
Nehmen wir an, unser Farbverlauf liegt zwischen #602749 und #b14623. Die Kanalwerte unserer Farbverlauf-Stopps sind 60 (R), 27 (G), 49 (B) und b1 (R), 46 (G), 23 (R), sodass der background hinter dem button Kanalwerte haben muss, die mindestens max(60, b1) = b1 für Rot, max(27, 46) = 46 für Grün und max(49, 23) = 49 für Blau sind.
Das bedeutet, dass wir einen background-color auf unserem button mit Kanalwerten haben, die größer oder gleich b1, 46 und 49 sind, entweder als eigenständiger fester background oder unter einer background-image-Ebene, die einen screen-Blendmodus verwendet (der immer ein Ergebnis erzeugt, das mindestens so hell ist wie die hellere der beiden Ebenen).
Wir müssen auch den button-Rand, den Text und das Icon black machen und gleichzeitig den background auf white setzen.
$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);
}
}
Das Icon im ::after-Pseudoelement wird black gemacht, indem filter: brightness(0) darauf angewendet wird.

Wir haben auch die Möglichkeit, alle button-Ebenen als Teil seines background zu überblenden, sowohl für das helle als auch für das dunkle Thema, aber wie bereits erwähnt, ignoriert Firefox jegliche background-clip-Deklaration, bei der text Teil einer Liste von Werten ist und nicht der einzige Wert.
Nun, das war's! Ich hoffe, Sie hatten (oder haben) ein gruseliges Halloween. Meine wurde definitiv durch all die Bugs, die ich entdeckt – oder wiederentdeckt – habe, sowie durch die Erkenntnis, dass sie inzwischen nicht behoben wurden, furchterregend gemacht.
Ein weiterer seltsamer Safari-Bug mit Text-Clipping: Ich glaube, er verwendet die CPU anstelle der GPU zum Rendern von Text-Clipping-Masken, was zu massiven Performance-Problemen führte, wann immer meine Seite mit einem Stück Farbverlauf-Text geladen wurde, selbst wenn das Element nicht sichtbar war.
Ich sehe mir diese Seite in Firefox an, und im Abschnitt "kompilierte Demo" funktioniert die "Extra pseudo cross-browser solution" nicht und zeigt nur den Hintergrund ohne Text oder Symbole an.
Ups! Jetzt behoben, danke für die Benachrichtigung!
Was ist mit dem :hover und :focus-Stil für diese Buttons?
Ich habe über einen Effekt nachgedacht, wie der, der beim Überfahren/Fokussieren der Links hier für den inneren Teil des Buttons vorhanden ist.
Also etwas wie das hier mitten im Übergang in unserem Fall.
Ich habe es nicht erwähnt, weil der Artikel bereits lang war und die gesamte Diskussion darüber, wie man es mit Maskenkomposition oder Blending zum Laufen bringt, recht komplex ist. Ich schreibe vielleicht einen Artikel nur darüber, aber es gibt so viele andere Dinge, über die ich auch gerne schreiben würde.