Es ist eine Frage, die ich ziemlich oft höre: Ist es möglich, Schatten aus Verläufen anstelle von Vollfarben zu erzeugen? Es gibt keine spezielle CSS-Eigenschaft, die das leistet (glauben Sie mir, ich habe gesucht) und jeder Blogbeitrag, den Sie dazu finden, ist im Grunde eine Menge CSS-Tricks, um einen Gradienten zu approximieren. Einige davon werden wir auch behandeln.
Aber zuerst... noch ein Artikel über Gradienten-Schatten? Wirklich?
Ja, dies ist ein weiterer Beitrag zu diesem Thema, aber er ist anders. Gemeinsam werden wir die Grenzen ausreizen, um eine Lösung zu finden, die etwas abdeckt, das ich noch nirgendwo anders gesehen habe: Transparenz. Die meisten Tricks funktionieren, wenn das Element einen nicht-transparenten Hintergrund hat, aber was ist, wenn wir einen transparenten Hintergrund haben? Diesen Fall werden wir hier untersuchen!
Bevor wir beginnen, lassen Sie mich meinen Generator für Gradienten-Schatten vorstellen. Sie müssen nur die Konfiguration anpassen und den Code erhalten. Aber bleiben Sie dabei, denn ich werde Ihnen helfen, die gesamte Logik hinter dem generierten Code zu verstehen.
Nicht-transparente Lösung
Beginnen wir mit der Lösung, die in 80 % der Fälle funktioniert. Der typischste Fall: Sie verwenden ein Element mit einem Hintergrund und müssen ihm einen Gradienten-Schatten hinzufügen. Hier gibt es keine Transparenzprobleme zu berücksichtigen.
Die Lösung besteht darin, sich auf ein Pseudoelement zu verlassen, in dem der Gradient definiert ist. Sie platzieren es hinter dem eigentlichen Element und wenden einen Weichzeichner-Filter darauf an.
.box {
position: relative;
}
.box::before {
content: "";
position: absolute;
inset: -5px; /* control the spread */
transform: translate(10px, 8px); /* control the offsets */
z-index: -1; /* place the element behind */
background: /* your gradient here */;
filter: blur(10px); /* control the blur */
}
Das sieht nach viel Code aus, und das ist es auch. So hätten wir es stattdessen mit einer box-shadow gemacht, wenn wir eine Vollfarbe anstelle eines Gradienten verwendet hätten.
box-shadow: 10px 8px 10px 5px orange;
Das sollte Ihnen eine gute Vorstellung davon geben, was die Werte im ersten Snippet bewirken. Wir haben X- und Y-Offsets, den Weichzeichner-Radius und die Streckweite. Beachten Sie, dass wir für die Streckweite einen negativen Wert benötigen, der von der inset-Eigenschaft kommt.
Hier ist eine Demo, die den Gradienten-Schatten neben einem klassischen box-shadow zeigt.
Wenn Sie genau hinschauen, werden Sie feststellen, dass beide Schatten ein wenig unterschiedlich sind, insbesondere der Weichzeichner-Teil. Das ist kein Wunder, denn ich bin ziemlich sicher, dass der Algorithmus der filter-Eigenschaft anders funktioniert als der für box-shadow. Das ist keine große Sache, da das Ergebnis letztendlich ziemlich ähnlich ist.
Diese Lösung ist gut, hat aber immer noch einige Nachteile im Zusammenhang mit der z-index: -1 Deklaration. Ja, es gibt "Stacking Context", der dort passiert!
Ich habe ein transform auf das Hauptelement angewendet, und zack! Der Schatten ist nicht mehr unter dem Element. Das ist kein Fehler, sondern das logische Ergebnis eines Stacking-Kontextes. Keine Sorge, ich werde keine langweilige Erklärung zum Stacking-Kontext beginnen (das habe ich bereits in einem Stack Overflow-Thread getan), aber ich werde Ihnen trotzdem zeigen, wie Sie damit umgehen können.
Die erste von mir empfohlene Lösung ist die Verwendung eines 3D-transform.
.box {
position: relative;
transform-style: preserve-3d;
}
.box::before {
content: "";
position: absolute;
inset: -5px;
transform: translate3d(10px, 8px, -1px); /* (X, Y, Z) */
background: /* .. */;
filter: blur(10px);
}
Anstatt z-index: -1 zu verwenden, werden wir eine negative Translation entlang der Z-Achse verwenden. Wir werden alles in translate3d() packen. Vergessen Sie nicht, transform-style: preserve-3d auf das Hauptelement anzuwenden, da sonst der 3D-transform nicht wirksam wird.
Soweit ich weiß, hat diese Lösung keine Nebenwirkungen... aber vielleicht sehen Sie ja eine. Wenn das der Fall ist, teilen Sie es im Kommentarbereich mit und lassen Sie uns versuchen, eine Lösung dafür zu finden!
Wenn Sie aus irgendeinem Grund keinen 3D-transform verwenden können, besteht die andere Lösung darin, sich auf zwei Pseudoelemente zu verlassen — ::before und ::after. Eines erzeugt den Gradienten-Schatten und das andere repliziert den Haupt-Hintergrund (und andere Stile, die Sie möglicherweise benötigen). Auf diese Weise können wir die Stapelreihenfolge beider Pseudoelemente einfach steuern.
.box {
position: relative;
z-index: 0; /* We force a stacking context */
}
/* Creates the shadow */
.box::before {
content: "";
position: absolute;
z-index: -2;
inset: -5px;
transform: translate(10px, 8px);
background: /* .. */;
filter: blur(10px);
}
/* Reproduces the main element styles */
.box::after {
content: """;
position: absolute;
z-index: -1;
inset: 0;
/* Inherit all the decorations defined on the main element */
background: inherit;
border: inherit;
box-shadow: inherit;
}
Es ist wichtig zu beachten, dass wir das Hauptelement zwingen, einen Stacking-Kontext zu erstellen, indem wir z-index: 0 oder eine andere Eigenschaft, die dasselbe bewirkt, darauf anwenden. Vergessen Sie auch nicht, dass Pseudoelemente die Padding-Box des Hauptelements als Referenz betrachten. Wenn das Hauptelement einen Rahmen hat, müssen Sie dies bei der Definition der Pseudoelement-Stile berücksichtigen. Sie werden feststellen, dass ich inset: -2px auf ::after verwende, um den auf dem Hauptelement definierten Rahmen zu berücksichtigen.
Wie gesagt, diese Lösung ist wahrscheinlich in den meisten Fällen, in denen Sie einen Gradienten-Schatten wünschen, gut genug, solange Sie keine Transparenz benötigen. Aber wir sind hier für die Herausforderung und um die Grenzen auszuloten, also bleiben Sie bei mir, auch wenn Sie das Folgende nicht brauchen. Sie werden wahrscheinlich neue CSS-Tricks lernen, die Sie anderswo verwenden können.
Transparente Lösung
Greifen wir dort auf, wo wir beim 3D-transform aufgehört haben, und entfernen wir den Hintergrund vom Hauptelement. Ich beginne mit einem Schatten, bei dem sowohl die Offsets als auch die Streckweite gleich 0 sind.
Die Idee ist, einen Weg zu finden, alles innerhalb des Bereichs des Elements (innerhalb des grünen Rahmens) zu schneiden oder zu verstecken, während das Äußere erhalten bleibt. Dafür werden wir clip-path verwenden. Aber Sie fragen sich vielleicht, wie clip-path einen Schnitt innerhalb eines Elements erzeugen kann.
In der Tat gibt es keine Möglichkeit, das zu tun, aber wir können es mit einem bestimmten Polygonmuster simulieren.
clip-path: polygon(-100vmax -100vmax,100vmax -100vmax,100vmax 100vmax,-100vmax 100vmax,-100vmax -100vmax,0 0,0 100%,100% 100%,100% 0,0 0)
Tada! Wir haben einen Gradienten-Schatten, der Transparenz unterstützt. Alles, was wir getan haben, ist, dem vorherigen Code ein clip-path hinzuzufügen. Hier ist eine Abbildung zur Veranschaulichung des Polygonteils.

Der blaue Bereich ist der sichtbare Teil nach Anwendung des clip-path. Ich verwende nur die blaue Farbe zur Veranschaulichung des Konzepts, aber in Wirklichkeit sehen wir nur den Schatten innerhalb dieses Bereichs. Wie Sie sehen können, haben wir vier Punkte, die mit einem großen Wert (B) definiert sind. Mein großer Wert ist 100vmax, aber es kann jeder große Wert sein, den Sie wünschen. Die Idee ist, sicherzustellen, dass wir genügend Platz für den Schatten haben. Wir haben auch vier Punkte, die die Ecken des Pseudoelements sind.
Die Pfeile veranschaulichen den Pfad, der das Polygon definiert. Wir starten bei (-B, -B) bis wir (0,0) erreichen. Insgesamt benötigen wir 10 Punkte. Nicht acht Punkte, weil zwei Punkte zweimal im Pfad wiederholt werden ((-B,-B) und (0,0)).
Es bleibt uns noch eine weitere Sache zu tun, und das ist die Berücksichtigung der Streckweite und der Offsets. Der einzige Grund, warum die obige Demo funktioniert, ist, dass es sich um einen Sonderfall handelt, bei dem Offsets und Streckweite gleich 0 sind.
Definieren wir die Streckung und sehen wir, was passiert. Denken Sie daran, dass wir inset mit einem negativen Wert verwenden, um dies zu tun.
Das Pseudoelement ist jetzt größer als das Hauptelement, sodass der clip-path mehr schneidet, als wir brauchen. Denken Sie daran, dass wir immer den Teil innerhalb des Hauptelements (den Bereich innerhalb des grünen Rahmens des Beispiels) schneiden müssen. Wir müssen die Position der vier Punkte innerhalb von clip-path anpassen.
.box {
--s: 10px; /* the spread */
position: relative;
}
.box::before {
inset: calc(-1 * var(--s));
clip-path: polygon(
-100vmax -100vmax,
100vmax -100vmax,
100vmax 100vmax,
-100vmax 100vmax,
-100vmax -100vmax,
calc(0px + var(--s)) calc(0px + var(--s)),
calc(0px + var(--s)) calc(100% - var(--s)),
calc(100% - var(--s)) calc(100% - var(--s)),
calc(100% - var(--s)) calc(0px + var(--s)),
calc(0px + var(--s)) calc(0px + var(--s))
);
}
Wir haben eine CSS-Variable, --s, für die Streckweite definiert und die Polygonpunkte aktualisiert. Ich habe die Punkte, bei denen ich den großen Wert verwende, nicht berührt. Ich aktualisiere nur die Punkte, die die Ecken des Pseudoelements definieren. Ich erhöhe alle Nullwerte um --s und verringere die 100%-Werte um --s.
Mit den Offsets ist es die gleiche Logik. Wenn wir das Pseudoelement verschieben, ist der Schatten außer der Reihe und wir müssen das Polygon erneut korrigieren und die Punkte in die entgegengesetzte Richtung verschieben.
.box {
--s: 10px; /* the spread */
--x: 10px; /* X offset */
--y: 8px; /* Y offset */
position: relative;
}
.box::before {
inset: calc(-1 * var(--s));
transform: translate3d(var(--x), var(--y), -1px);
clip-path: polygon(
-100vmax -100vmax,
100vmax -100vmax,
100vmax 100vmax,
-100vmax 100vmax,
-100vmax -100vmax,
calc(0px + var(--s) - var(--x)) calc(0px + var(--s) - var(--y)),
calc(0px + var(--s) - var(--x)) calc(100% - var(--s) - var(--y)),
calc(100% - var(--s) - var(--x)) calc(100% - var(--s) - var(--y)),
calc(100% - var(--s) - var(--x)) calc(0px + var(--s) - var(--y)),
calc(0px + var(--s) - var(--x)) calc(0px + var(--s) - var(--y))
);
}
Es gibt zwei weitere Variablen für die Offsets: --x und --y. Wir verwenden sie innerhalb von transform und aktualisieren auch die clip-path-Werte. Wir berühren immer noch nicht die Polygonpunkte mit großen Werten, aber wir verschieben alle anderen — wir reduzieren --x von den X-Koordinaten und --y von den Y-Koordinaten.
Jetzt müssen wir nur noch ein paar Variablen aktualisieren, um den Gradienten-Schatten zu steuern. Und während wir dabei sind, machen wir auch den Weichzeichner-Radius zu einer Variablen.
Benötigen wir den Trick mit dem 3D-
transformnoch?
Das hängt vom Rahmen ab. Vergessen Sie nicht, dass die Referenz für ein Pseudoelement die Padding-Box ist. Wenn Sie Ihrem Hauptelement einen Rahmen hinzufügen, kommt es zu einer Überlappung. Sie behalten entweder den 3D-transform-Trick bei oder aktualisieren den inset-Wert, um den Rahmen zu berücksichtigen.
Hier ist die vorherige Demo mit einem aktualisierten inset-Wert anstelle des 3D-transform.
Ich würde sagen, das ist ein besserer Ansatz, da die Streckweite genauer ist, da sie vom border-box statt vom padding-box ausgeht. Aber Sie müssen den inset-Wert entsprechend dem Rahmen des Hauptelements anpassen. Manchmal ist der Rahmen des Elements unbekannt und Sie müssen die vorherige Lösung verwenden.
Mit der früheren nicht-transparenten Lösung können Sie auf ein Stacking-Kontext-Problem stoßen. Und mit der transparenten Lösung können Sie stattdessen auf ein Rahmen-Problem stoßen. Jetzt haben Sie Optionen und Möglichkeiten, diese Probleme zu umgehen. Der 3D-Transform-Trick ist meine bevorzugte Lösung, da er alle Probleme behebt (Der Online-Generator wird ihn auch berücksichtigen).
Hinzufügen eines abgerundeten Ecken
Wenn Sie versuchen, border-radius zum Element hinzuzufügen, wenn Sie die nicht-transparente Lösung verwenden, mit der wir begonnen haben, ist das eine ziemlich triviale Aufgabe. Alles, was Sie tun müssen, ist, denselben Wert vom Hauptelement zu erben, und Sie sind fertig.
Auch wenn Sie keinen abgerundeten Ecken haben, ist es eine gute Idee, border-radius: inherit zu definieren. Das berücksichtigt jeden potenziellen border-radius, den Sie später hinzufügen möchten, oder einen abgerundeten Ecken, der von woanders kommt.
Es ist eine andere Geschichte, wenn es um die transparente Lösung geht. Leider bedeutet das, eine andere Lösung zu finden, da clip-path nicht mit Kurven umgehen kann. Das bedeutet, dass wir den Bereich innerhalb des Hauptelements nicht schneiden können.
Wir werden die mask-Eigenschaft einführen.
Dieser Teil war sehr mühsam, und ich hatte Mühe, eine allgemeine Lösung zu finden, die nicht auf magische Zahlen angewiesen ist. Ich landete bei einer sehr komplexen Lösung, die nur ein Pseudoelement verwendet, aber der Code war ein Klumpen Spaghetti, der nur wenige spezielle Fälle abdeckte. Ich glaube nicht, dass es sich lohnt, diesen Weg zu erkunden.
Ich habe mich entschieden, ein zusätzliches Element einzufügen, um den Code einfacher zu gestalten. Hier ist das Markup:
<div class="box">
<sh></sh>
</div>
Ich verwende ein benutzerdefiniertes Element, <sh>, um mögliche Konflikte mit externem CSS zu vermeiden. Ich hätte auch ein <div> verwenden können, aber da es sich um ein gängiges Element handelt, kann es leicht von einer anderen CSS-Regel von woanders angesprochen werden, die unseren Code brechen kann.
Der erste Schritt ist, das <sh>-Element zu positionieren und absichtlich einen Überlauf zu erzeugen.
.box {
--r: 50px;
position: relative;
border-radius: var(--r);
}
.box sh {
position: absolute;
inset: -150px;
border: 150px solid #0000;
border-radius: calc(150px + var(--r));
}
Der Code mag etwas seltsam aussehen, aber wir werden die Logik dahinter im Laufe der Zeit erläutern. Als nächstes erstellen wir den Gradienten-Schatten mit einem Pseudoelement von <sh>.
.box {
--r: 50px;
position: relative;
border-radius: var(--r);
transform-style: preserve-3d;
}
.box sh {
position: absolute;
inset: -150px;
border: 150px solid #0000;
border-radius: calc(150px + var(--r));
transform: translateZ(-1px)
}
.box sh::before {
content: "";
position: absolute;
inset: -5px;
border-radius: var(--r);
background: /* Your gradient */;
filter: blur(10px);
transform: translate(10px,8px);
}
Wie Sie sehen können, verwendet das Pseudoelement denselben Code wie in allen vorherigen Beispielen. Der einzige Unterschied ist der 3D-transform, der auf dem <sh>-Element anstelle des Pseudoelements definiert ist. Vorerst haben wir einen Gradienten-Schatten ohne die Transparenzfunktion.
Beachten Sie, dass der Bereich des <sh>-Elements mit dem schwarzen Umriss definiert ist. Warum mache ich das? Weil ich dadurch eine mask darauf anwenden kann, um den Teil innerhalb des grünen Bereichs zu verbergen und den überlaufenden Teil beizubehalten, wo wir den Schatten sehen müssen.
Ich weiß, es ist ein bisschen knifflig, aber im Gegensatz zu clip-path berücksichtigt die mask-Eigenschaft nicht den Bereich außerhalb eines Elements, um Dinge anzuzeigen und zu verbergen. Deshalb war ich gezwungen, das zusätzliche Element einzuführen, um den "Außenbereich" zu simulieren.
Beachten Sie auch, dass ich eine Kombination aus border und inset verwende, um diesen Bereich zu definieren. Dadurch kann ich die Padding-Box dieses zusätzlichen Elements gleich dem Hauptelement halten, sodass das Pseudoelement keine zusätzlichen Berechnungen benötigt.
Ein weiterer nützlicher Vorteil der Verwendung eines zusätzlichen Elements ist, dass das Element fixiert ist und nur das Pseudoelement sich bewegt (mittels translate). Dies ermöglicht es mir, die Maske einfach zu definieren, was der letzte Schritt dieses Tricks ist.
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
Fertig! Wir haben unseren Gradienten-Schatten, und er unterstützt border-radius! Sie haben wahrscheinlich einen komplexen mask-Wert mit vielen Gradienten erwartet, aber nein! Wir brauchen nur zwei einfache Gradienten und eine mask-composite, um die Magie zu vollenden.
Lassen Sie uns das <sh>-Element isolieren, um zu verstehen, was dort passiert.
.box sh {
position: absolute;
inset: -150px;
border: 150px solid red;
background: lightblue;
border-radius: calc(150px + var(--r));
}
Hier ist, was wir bekommen
Beachten Sie, wie der innere Radius dem border-radius des Hauptelements entspricht. Ich habe einen großen Rahmen (150px) und einen border-radius definiert, der dem großen Rahmen plus dem Radius des Hauptelements entspricht. Außen habe ich einen Radius von 150px + R. Innen habe ich 150px + R - 150px = R.
Wir müssen den inneren (blauen) Teil verbergen und sicherstellen, dass der Rahmen- (rote) Teil sichtbar bleibt. Dazu habe ich zwei Masken-Ebenen definiert — eine, die nur den Content-Box-Bereich abdeckt, und eine andere, die den Border-Box-Bereich (Standardwert) abdeckt. Dann habe ich eine von der anderen subtrahiert, um den Rahmen freizulegen.
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
Ich habe die gleiche Technik verwendet, um einen Rahmen zu erstellen, der Gradienten und border-radius unterstützt. Ana Tudor hat auch einen guten Artikel über Masken-Komposition, den ich Ihnen zu lesen empfehle.
Gibt es Nachteile bei dieser Methode?
Ja, das ist definitiv nicht perfekt. Das erste Problem, auf das Sie stoßen könnten, betrifft die Verwendung eines Rahmens für das Hauptelement. Dies kann zu einer leichten Fehlausrichtung der Radien führen, wenn Sie es nicht berücksichtigen. Dieses Problem haben wir in unserem Beispiel, aber vielleicht bemerken Sie es kaum.
Die Behebung ist relativ einfach: Addieren Sie die Rahmenbreite zum inset des <sh>-Elements.
.box {
--r: 50px;
border-radius: var(--r);
border: 2px solid;
}
.box sh {
position: absolute;
inset: -152px; /* 150px + 2px */
border: 150px solid #0000;
border-radius: calc(150px + var(--r));
}
Ein weiterer Nachteil ist der große Wert, den wir für den Rahmen verwenden (150px im Beispiel). Dieser Wert sollte groß genug sein, um den Schatten zu umschließen, aber nicht zu groß, um Überläufe und Scrollbalken-Probleme zu vermeiden. Glücklicherweise berechnet der Online-Generator den optimalen Wert unter Berücksichtigung aller Parameter.
Der letzte Nachteil, der mir bekannt ist, tritt auf, wenn Sie mit einem komplexen border-radius arbeiten. Wenn Sie beispielsweise für jede Ecke einen anderen Radius anwenden möchten, müssen Sie für jede Seite eine Variable definieren. Das ist eigentlich kein Nachteil, denke ich, aber es kann Ihren Code etwas schwieriger zu warten machen.
.box {
--r-top: 10px;
--r-right: 40px;
--r-bottom: 30px;
--r-left: 20px;
border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left);
}
.box sh {
border-radius: calc(150px + var(--r-top)) calc(150px + var(--r-right)) calc(150px + var(--r-bottom)) calc(150px + var(--r-left));
}
.box sh:before {
border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left);
}
Der Online-Generator berücksichtigt der Einfachheit halber nur einen einheitlichen Radius, aber Sie wissen jetzt, wie Sie den Code ändern können, wenn Sie eine komplexe Radiuskonfiguration berücksichtigen möchten.
Zusammenfassung
Wir sind am Ende angekommen! Die Magie hinter Gradienten-Schatten ist kein Geheimnis mehr. Ich habe versucht, alle Möglichkeiten und alle möglichen Probleme abzudecken, auf die Sie stoßen könnten. Wenn ich etwas übersehen habe oder Sie auf ein Problem stoßen, melden Sie es bitte im Kommentarbereich, und ich werde es überprüfen.
Auch hier ist vieles wahrscheinlich übertrieben, wenn man bedenkt, dass die de facto Lösung die meisten Ihrer Anwendungsfälle abdecken wird. Dennoch ist es gut, das "Warum" und "Wie" hinter dem Trick zu kennen und wie man seine Grenzen überwindet. Außerdem haben wir gutes Training im Umgang mit CSS-Clipping und Masking bekommen.
Und natürlich haben Sie den Online-Generator, auf den Sie jederzeit zurückgreifen können, um den Aufwand zu vermeiden.
Ich hatte im Laufe der Jahre mehrmals Schwierigkeiten mit einfachen Transformationen an Pseudoelementen, die das z-Indexing brachen. Ich bin ziemlich zuversichtlich in meine CSS-Fähigkeiten und habe ein gutes Verständnis von Stacking-Kontexten, aber der obige Satz war wie eine Tüte Pop-Rocks, die über mein Gehirn gestreut wurde; Plötzlich erinnerte ich mich an jeden Moment, in dem diese Lösung funktioniert hätte.
Immer wieder großartig, einen neuen Ansatz für ein ähnliches Ergebnis zu lernen.