CSS-ing Candy Ghost Buttons

Avatar of Ana Tudor
Ana Tudor am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

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

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.
Die reinen CSS Candy Ghost Buttons.

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.

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.
Das Ergebnis nach Anwendung der Basisstile (Live-Demo).

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.

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.
Gradient-Overlay vs. derselbe Gradientenmaske (Live-Demo).

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.

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.
Die Layout-Boxen (Live-Demo).

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.

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.
Die beiden Basisebenen (Live-Demo).

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.

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.
Das Ergebnis nach dem Ausmaskieren der gesamten 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;
  }
}
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.
Das Endergebnis mit der reinen Maskierungsmethode (Live-Demo).

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.

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.
Knockout-Button-Text (Live-Demo).

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!

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.
Das Ergebnis nach dem Hinzufügen des Farbverlauf-Pseudoelements (Live-Demo).

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.

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.
Das Endergebnis mit der zusätzlichen Pseudoelement-Methode (Live-Demo).

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).

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.
Das Ergebnis ohne Eckabrundung mit der 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

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.
Wie die 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.

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.
Das Ergebnis mit kleiner Eckabrundung mit der 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.

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.
Die All-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.

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.
Die Cover-Lösung auf einem Pseudoelement für einen soliden 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
  }
}
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.
Die Cover-Lösung auf einem Pseudoelement für einen festen 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.

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.
Der schwarze und weiße Button (Live-Demo).

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.

Screenshot. Shows a pill-shaped gradient button with no visible text on top of a dark image background.
Das Ergebnis nach dem Hinzufügen eines Farbverlauf-Pseudoelements über dem schwarzen und weißen Button (Live-Demo).

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;
  }
}
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.
Das „fast fertige“ Ergebnis (Live-Demo).

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;
}
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.
Der helle Ghost-Button über einem dunklen Hintergrund (Live-Demo).

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.

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.
Der dunkle Ghost-Button über einem hellen Hintergrund (Live-Demo).

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.