Die Idee dazu kam mir, als ich dieses Farbverlauf-Unendlichkeitslogo von Infographic Paradise sah.

Nach vier Stunden und etwa zwanzig Minuten, von denen über vier Stunden für die Feinabstimmung von Positionierung, Kanten und Lichtern aufgewendet wurden… hatte ich schließlich das folgende Ergebnis:

Der Farbverlauf sieht nicht wie in der Originalillustration aus, da ich mich entschieden habe, den Regenbogen logisch zu generieren, anstatt den Dev Tools Picker oder so etwas zu verwenden. Aber ansonsten denke ich, dass ich ziemlich nah dran bin – also lasst uns sehen, wie ich es gemacht habe!
Markup
Wie Sie wahrscheinlich schon am Titel erraten haben, besteht das HTML nur aus einem Element.
<div class='∞'></div>
Styling
Entscheidung für den Ansatz
Der erste Gedanke, der einem beim Anblick des obigen Beispiels in den Sinn kommen könnte, ist die Verwendung von konischen Farbverläufen als Randbilder. Leider spielen border-image und border-radius nicht gut zusammen, wie die interaktive Demo unten zeigt.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Immer wenn wir ein border-image setzen, wird border-radius einfach ignoriert, sodass die gemeinsame Verwendung von beiden leider keine Option ist.
Der hier verwendete Ansatz ist also die Verwendung von conic-gradient() Hintergründen, um dann den mittleren Teil mithilfe einer mask zu entfernen. Schauen wir uns an, wie das funktioniert!
Erstellen der beiden ∞ Hälften
Wir legen zuerst einen Außendurchmesser fest.
$do: 12.5em;
Wir erstellen die beiden Hälften des Unendlichkeitszeichens mithilfe der Pseudo-Elemente ::before und ::after unseres .∞ Elements. Um diese beiden Pseudo-Elemente nebeneinander zu platzieren, verwenden wir ein Flex-Layout für ihr übergeordnetes Element (das Unendlichkeits-Element .∞). Jedes davon hat sowohl width als auch height gleich dem Außendurchmesser $do. Wir runden sie außerdem mit einem border-radius von 50% ab und geben ihnen einen Dummy-background, damit wir sie sehen können.
.∞ {
display: flex;
&:before, &:after {
width: $do; height: $do;
border-radius: 50%;
background: #000;
content: '';
}
}
Wir haben das .∞ Element auch vertikal und horizontal in der Mitte seines übergeordneten Elements (in diesem Fall des body) platziert, indem wir den Flexbox-Ansatz verwenden.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Wie conic-gradient() funktioniert
Um die conic-gradient() Hintergründe für die beiden Hälften zu erstellen, müssen wir zuerst verstehen, wie die conic-gradient() Funktion funktioniert.
Wenn innerhalb der conic-gradient() Funktion eine Liste von Stopps ohne explizite Positionen steht, wird der erste als bei 0% (oder 0deg, dasselbe) und der letzte als bei 100% (oder 360deg) betrachtet, während alle übrigen gleichmäßig im Intervall [0%, 100%] verteilt werden.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Wenn wir nur 2 Stopps haben, ist es einfach. Der erste ist bei 0%, der zweite (und letzte) bei 100% und dazwischen liegen keine weiteren Stopps.
Wenn wir 3 Stopps haben, ist der erste bei 0%, der letzte (dritte) bei 100%, während der zweite genau in der Mitte des Intervalls [0%, 100%], also bei 50% liegt.
Wenn wir 4 Stopps haben, ist der erste bei 0%, der letzte (vierte) bei 100%, während der zweite und dritte das Intervall [0%, 100%] in 3 gleiche Intervalle teilen und bei 33.(3)% bzw. 66.(6)% positioniert sind.
Wenn wir 5 Stopps haben, ist der erste bei 0%, der letzte (fünfte) bei 100%, während der zweite, dritte und vierte das Intervall [0%, 100%] in 4 gleiche Intervalle teilen und bei 25%, 50% und 75% positioniert sind.
Wenn wir 6 Stopps haben, ist der erste bei 0%, der letzte (sechste) bei 100%, während der zweite, dritte, vierte und fünfte das Intervall [0%, 100%] in 5 gleiche Intervalle teilen und bei 20%, 40%, 60% und 80% positioniert sind.
Im Allgemeinen, wenn wir n Stopps haben, ist der erste bei 0%, der letzte bei 100%, während die dazwischen liegenden das Intervall [0%, 100%] in n-1 gleiche Intervalle teilen, die jeweils 100%/(n-1) umfassen. Wenn wir den Stopps 0-basierte Indizes geben, dann ist jeder von ihnen bei i*100%/(n-1) positioniert.
Für den ersten ist i gleich 0, was uns 0*100%/(n-1) = 0% ergibt.
Für den letzten (n-ten) ist i gleich n-1, was uns (n-1)*100%/(n-1) = 100% ergibt.
Hier entscheiden wir uns für 9 Stopps, was bedeutet, dass wir das Intervall [0%, 100%] in 8 gleiche Intervalle aufteilen.
In Ordnung, aber wie kommen wir an die Stopp-Liste?
Die HSL()-Stopps
Nun, zur Einfachheit wählen wir, sie als Liste von HSL-Werten zu generieren. Wir halten die Sättigung und die Helligkeit konstant und variieren den Farbton. Der Farbton ist ein Winkelwert, der von 0 bis 360 reicht, wie wir hier sehen können.

0 bis 360 (Sättigung und Helligkeit bleiben konstant).Vor diesem Hintergrund können wir eine Liste von hsl() Stopps mit fester Sättigung und Helligkeit sowie variablem Farbton erstellen, wenn wir den Start-Farbton $hue-start, den Farbtonbereich $hue-range (dies ist der End-Farbton minus der Start-Farbton) und die Anzahl der Stopps $num-stops kennen.
Nehmen wir an, wir halten die Sättigung und die Helligkeit bei 85% bzw. 57% fest (willkürliche Werte, die wahrscheinlich für bessere Ergebnisse angepasst werden können) und gehen zum Beispiel von einem Start-Farbton von 240 zu einem End-Farbton von 300 und verwenden 4 Stopps.
Um diese Liste von Stopps zu generieren, verwenden wir eine get-stops() Funktion, die diese drei Dinge als Argumente nimmt.
@function get-stops($hue-start, $hue-range, $num-stops) {}
Wir erstellen die Liste der Stopps $list, die ursprünglich leer ist (und die wir am Ende zurückgeben werden, nachdem wir sie gefüllt haben). Wir berechnen auch die Spanne eines der gleichen Intervalle, in die unsere Stopps das volle Intervall von Start bis Ende aufteilen ($unit).
@function get-stops($hue-start, $hue-range, $num-stops) {
$list: ();
$unit: $hue-range/($num-stops - 1);
/* populate the list of stops $list */
@return $list
}
Um unser $list zu füllen, durchlaufen wir die Stopps, berechnen den aktuellen Farbton, verwenden den aktuellen Farbton, um den hsl() Wert an diesem Stopp zu generieren, und fügen ihn dann der Liste der Stopps hinzu.
@for $i from 0 to $num-stops {
$hue-curr: $hue-start + $i*$unit;
$list: $list, hsl($hue-curr, 85%, 57%);
}
Wir können die Stopp-Liste, die diese Funktion zurückgibt, nun für jede Art von Farbverlauf verwenden, wie aus den Nutzungsbeispielen für diese Funktion in der interaktiven Demo unten ersichtlich ist (die Navigation funktioniert sowohl über die Vorwärts-/Rückwärts-Pfeile an den Seiten als auch über die Pfeiltasten und die Tasten Bildlauf nach unten / Bildlauf nach oben).
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Beachten Sie, dass, wenn unser Bereich ein Ende des Intervalls [0, 360] überschreitet, er vom anderen Ende fortgesetzt wird. Wenn zum Beispiel der Start-Farbton 30 und der Bereich -210 ist (das vierte Beispiel), können wir nur bis 0 gehen, und dann gehen wir weiter von 360 abwärts.
Konische Farbverläufe für unsere beiden Hälften
In Ordnung, aber wie bestimmen wir $hue-start und $hue-range für unseren speziellen Fall?
Im Originalbild ziehen wir eine Linie zwischen den Mittelpunkten der beiden Hälften der Schleife und sehen, beginnend mit dieser Linie, im Uhrzeigersinn in beiden Fällen, wo wir starten und wo wir im Farbton-Intervall [0, 360] enden und welche anderen Farbtöne wir passieren.

Um die Dinge zu vereinfachen, betrachten wir, dass wir die gesamte Farbtonskala [0, 360] entlang unseres Unendlichkeitszeichens durchlaufen. Das bedeutet, dass der Bereich für jede Hälfte absolut 180 (die Hälfte von 360) beträgt.

100% bzw. 50% festgelegt sind.Auf der linken Hälfte beginnen wir bei etwas, das wie eine Mischung aus Cyan (Farbton 180) und Hellgrün (Farbton 120) aussieht, also nehmen wir den Start-Farbton als Durchschnitt der Farbtöne dieser beiden (180 + 120)/2 = 150.

Wir erreichen eine Art Rot, das 180 vom Startwert entfernt ist, also bei 330, egal ob wir 180 subtrahieren oder addieren.
(150 - 180 + 360)%360 = (150 + 180 + 360)%360 = 330
Also… gehen wir rauf oder runter? Nun, wir durchlaufen Gelbtöne, die auf der Farbtonskala um 60 liegen, also gehen wir von 150 abwärts, nicht aufwärts. Abwärts gehen bedeutet, dass unser Bereich negativ ist (-180).

Auf der rechten Hälfte beginnen wir ebenfalls mit demselben Farbton zwischen Cyan und Hellgrün (150) und enden auch mit derselben Art von Rot (330), aber diesmal durchlaufen wir Blautöne, die um 240 liegen, was bedeutet, dass wir von unserem Start-Farbton von 150 aufwärts gehen, sodass unser Bereich in diesem Fall positiv ist (180).
Was die Anzahl der Stopps betrifft, so sollten 9 ausreichen.
Aktualisieren wir nun unseren Code und verwenden die Werte für die linke Hälfte als Standardwerte für unsere Funktion.
@function get-stops($hue-start: 150, $hue-range: -180, $num-stops: 9) {
/* same as before */
}
.∞ {
display: flex;
&:before, &:after {
/* same as before */
background: conic-gradient(get-stops());
}
&:after {
background: conic-gradient(get-stops(150, 180));
}
}
Und jetzt haben unsere beiden Kreisscheiben conic-gradient() Hintergründe.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Wir möchten jedoch nicht, dass diese konischen Farbverläufe von oben beginnen.
Für die erste Kreisscheibe soll sie von rechts beginnen – das ist 90° von oben in der positiven (Uhrzeigersinn-) Richtung. Für die zweite Kreisscheibe soll sie von links beginnen – das ist 90° von oben in der anderen (negativen) Richtung, was 270° von oben in der Uhrzeigersinn-Richtung entspricht (da negative Winkel aus irgendeinem Grund nicht zu funktionieren scheinen).

Ändern wir unseren Code, um dies zu erreichen.
.∞ {
display: flex;
&:before, &:after {
/* same as before */
background: conic-gradient(from 90deg, get-stops());
}
&:after {
background: conic-gradient(from 270deg, get-stops(150, 180));
}
}
Bis jetzt alles gut!
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Von 🥧 zu 🍩
Der nächste Schritt ist, Löcher aus unseren beiden Hälften auszuschneiden. Wir tun dies mit einer mask oder, genauer gesagt, mit einer radial-gradient() Maske. Dies schließt derzeit die Kantenunterstützung aus, aber da dies etwas ist, das sich in Entwicklung befindet, wird es wahrscheinlich irgendwann in naher Zukunft eine browserübergreifende Lösung sein.
Denken Sie daran, dass CSS-Gradientenmasken standardmäßig alpha-Masken sind (und nur Firefox erlaubt derzeit die Änderung über mask-mode), was bedeutet, dass nur der Alphakanal zählt. Das Überlagern der mask über unserem Element lässt jedes Pixel dieses Elements den alpha-Kanal des entsprechenden Pixels der mask verwenden. Wenn das mask-Pixel vollständig transparent ist (sein alpha-Wert ist 0), dann ist es auch das entsprechende Pixel des Elements.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Um die mask zu erstellen, berechnen wir den Außenradius $ro (die Hälfte des Außendurchmessers $do) und den Innenradius $ri (ein Bruchteil des Außenradius $ro).
$ro: .5*$do;
$ri: .52*$ro;
$m: radial-gradient(transparent $ri, red 0);
Wir setzen dann die mask auf unseren beiden Hälften.
.∞ {
/* same as before */
&:before, &:after {
/* same as before */
mask: $m;
}
}
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Das sieht in Firefox perfekt aus, aber die Kanten von radialen Farbverläufen mit abrupten Übergängen von einem Stopp zum nächsten sehen in Chrome unschön aus, und folglich auch die inneren Kanten unserer Ringe.

Die Lösung hier wäre, keinen abrupten Übergang zwischen den Stopps zu haben, sondern ihn über eine kleine Distanz zu verteilen, sagen wir eine halbe Pixel.
$m: radial-gradient(transparent calc(#{$ri} - .5px), red $ri);
Wir haben nun die gezackten Kanten in Chrome beseitigt.

Der nächste Schritt ist, die beiden Hälften so zu versetzen, dass sie tatsächlich ein Unendlichkeitszeichen bilden. Die sichtbaren kreisförmigen Streifen haben beide die gleiche Breite, die Differenz zwischen dem Außenradius $ro und dem Innenradius $ri. Das bedeutet, wir müssen jede lateral um die Hälfte dieser Differenz $ri - $ri verschieben.
.∞ {
/* same as before */
&:before, &:after {
/* same as before */
margin: 0 (-.5*($ro - $ri));
}
}
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Sich überschneidende Hälften
Wir kommen näher, aber wir haben hier immer noch ein sehr großes Problem. Wir möchten nicht, dass der rechte Teil der Schleife vollständig über dem linken liegt. Stattdessen möchten wir, dass die obere Hälfte des rechten Teils über der des linken Teils liegt und die untere Hälfte des linken Teils über der des rechten Teils.
Wie erreichen wir das also?
Wir verfolgen einen ähnlichen Ansatz wie in einem älteren Artikel: Wir verwenden 3D!
Um besser zu verstehen, wie das funktioniert, betrachten wir das folgende Beispiel mit zwei Karten. Wenn wir sie um ihre x-Achsen drehen, befinden sie sich nicht mehr in der Ebene des Bildschirms. Eine positive Drehung bringt die Unterseite nach vorne und schiebt die Oberseite nach hinten. Eine negative Drehung bringt die Oberseite nach vorne und schiebt die Unterseite nach hinten.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Beachten Sie, dass die obige Demo in Edge nicht funktioniert.
Wenn wir also der linken einen positiven Dreh- und der rechten einen negativen Dreh-Effekt geben, erscheint die obere Hälfte der rechten vor der oberen Hälfte der linken und umgekehrt für die unteren Hälften.
Das Hinzufügen von perspective lässt das, was näher an unseren Augen ist, größer erscheinen und das, was weiter weg ist, kleiner, und wir verwenden viel kleinere Winkel. Ohne perspective haben wir die 3D-Ebenenüberschneidung ohne das 3D-Erscheinungsbild.
Beachten Sie, dass beide Hälften im selben 3D-Kontext sein müssen, was durch Setzen von transform-style: preserve-3d auf dem .∞ Element erreicht wird.
.∞ {
/* same as before */
transform-style: preserve-3d;
&:before, &:after {
/* same as before */
transform: rotatex(1deg);
}
&:after {
/* same as before */
transform: rotatex(-1deg);
}
}
Und jetzt sind wir fast am Ziel, aber noch nicht ganz.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Feinabstimmung
Wir haben einen kleinen rötlichen Streifen in der Mitte, weil der Farbverlauf endet und die Schnittlinie nicht ganz übereinstimmen.

Eine ziemlich unschöne, aber effektive Lösung ist, eine 1px Verschiebung vor der Drehung am rechten Teil (dem Pseudo-Element ::after) hinzuzufügen.
.∞:after { transform: translate(1px) rotatex(-1deg) }
Viel besser!
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Das ist aber immer noch nicht perfekt. Da die inneren Kanten unserer beiden Ringe etwas verschwommen sind, sieht der Übergang zwischen ihnen und den scharfen äußeren Kanten etwas seltsam aus. Vielleicht können wir das verbessern.

Eine schnelle Lösung wäre hier, eine radial-gradient() Abdeckung auf jede der beiden Hälften zu legen. Diese Abdeckung ist transparentweiß (rgba(#fff, 0)) für den größten Teil des nicht maskierten Bereichs der beiden Hälften und geht zu durchgehend weiß (rgba(#fff, 1)) entlang sowohl ihrer inneren als auch ihrer äußeren Kanten, sodass wir eine schöne Kontinuität haben.
$gc: radial-gradient(#fff $ri, rgba(#fff, 0) calc(#{$ri} + 1px),
rgba(#fff, 0) calc(#{$ro} - 1px), #fff calc(#{$ro} - .5px));
.∞ {
/* same as before */
&:before, &:after {
/* same as before */
background: $gc, conic-gradient(from 90deg, get-stops());
}
&:after {
/* same as before */
background: $gc, conic-gradient(from 270deg, get-stops(150, 180));
}
}
Der Vorteil wird deutlicher, sobald wir der body einen dunklen background hinzufügen.
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Jetzt sieht es auch beim Hineinzoomen besser aus.

Das Endergebnis
Schließlich fügen wir einige verschönernde Details hinzu, indem wir mehr subtile radiale Farbverlauf-Lichter über die beiden Hälften legen. Dies war der Teil, der mich am meisten beschäftigte, da er am wenigsten Logik und am meisten Versuch und Irrtum erforderte. Zu diesem Zeitpunkt habe ich einfach das Originalbild unter das .∞ Element gelegt, die beiden Hälften halbtransparent gemacht und angefangen, Farbverläufe hinzuzufügen und sie zu verfeinern, bis sie ziemlich gut den Lichtern entsprachen. Und man sieht, wann ich es leid wurde, weil die Positionsangaben zu groben Annäherungen mit wenigen Dezimalstellen werden.
Eine weitere coole Ergänzung wären Schlagschatten auf dem Ganzen mit einem filter auf dem body. Leider bricht dies den 3D-Schnitt-Effekt in Firefox, was bedeutet, dass wir ihn dort auch nicht hinzufügen können.
@supports not (-moz-transform: scale(2)) {
filter: drop-shadow(.25em .25em .25em #000)
drop-shadow(.25em .25em .5em #000);
}
Wir haben nun das endgültige statische Ergebnis!
Siehe den Pen von thebabydino (@thebabydino) auf CodePen.
Aufpeppen mit Animation!
Als ich diese Demo zum ersten Mal teilte, wurde ich nach Animationen gefragt. Zuerst dachte ich, das wäre kompliziert, aber dann fiel mir ein, dass es dank Houdini nicht so sein muss!
Wie in meinem früheren Artikel erwähnt, können wir zwischen Stopps animieren, sagen wir von Rot zu Blau. In unserem Fall bleiben die Sättigungs- und Helligkeitskomponenten der hsl() Werte, die zur Erzeugung des Regenbogenverlaufs verwendet werden, konstant, es ändert sich nur der Farbton.
Für jeden einzelnen Stopp geht der Farbton von seinem Anfangswert zu seinem Anfangswert plus 360, durchläuft dabei die gesamte Farbtonskala. Dies entspricht dem Beibehalten des Anfangsfarbtons und dem Variieren eines Offsets. Dieser Offset --off ist die benutzerdefinierte Eigenschaft, die wir animieren.
Leider bedeutet dies, dass die Unterstützung auf Blink-Browser mit aktiviertem Flag Experimental Web Platform features beschränkt ist. Während conic-gradient() ab Chrome 69 nativ ohne das Flag unterstützt wird, gilt dies nicht für Houdini, sodass in diesem speziellen Fall bisher kein wirklicher Vorteil erzielt wurde.

Dennoch wollen wir sehen, wie wir das alles in Code umsetzen!
Zunächst modifizieren wir die get-stops() Funktion so, dass der aktuelle Farbton zu jedem Zeitpunkt der Anfangsfarbton des aktuellen Stopps $hue-curr plus unser Offset --off ist.
$list: $list, hsl(calc(#{$hue-curr} + var(--off, 0)), 85%, 57%);
Als nächstes registrieren wir diese benutzerdefinierte Eigenschaft.
CSS.registerProperty({
name: '--off',
syntax: '<number>',
initialValue: 0,
inherits: true
})
Beachten Sie, dass inherits jetzt erforderlich ist, obwohl es in früheren Versionen der Spezifikation optional war.
Und schließlich animieren wir sie auf 360.
.∞ {
/* same as before */
&:before, &:after {
/* same as before */
animation: shift 2s linear infinite;
}
}
@keyframes shift { to { --off: 360 } }
Das gibt uns unsere animierte Gradienten-Unendlichkeit!
Das war's! Ich hoffe, Sie haben diesen Einblick in das genossen, was heutzutage mit CSS möglich ist!
Ich weiß nicht warum, aber der 3D-Trick funktioniert nicht in Chrome Android (soweit ich weiß, ist es die neueste stabile Version).
Mozilla/5.0 (Linux; Android 7.0; E5823 Build/32.3.A.2.33) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Mobile Safari/537.36
Sehr schön! Das könnte zu einer netten Ladeanimationskomponente gemacht werden.
Das ist großartig, super Arbeit.