Benutzerdefinierte CSS-Eigenschaften in der Kaskade
Veröffentlicht: 2022-03-10Letzten Monat hatte ich ein Gespräch auf Twitter über den Unterschied zwischen „bereichsbezogenen“ Stilen (die in einem Build-Prozess generiert werden) und „verschachtelten“ Stilen, die in CSS nativ sind. Ich habe gefragt, warum Entwickler anekdotisch die Besonderheit von ID-Selektoren vermeiden, während sie von JavaScript generierte „bereichsbezogene Stile“ annehmen? Keith Grant schlug vor, dass der Unterschied darin liegt, die Kaskade* und die Vererbung auszugleichen, dh der Nähe der Spezifität den Vorzug zu geben. Lass uns einen Blick darauf werfen.
Die Kaskade
Die CSS-Kaskade basiert auf drei Faktoren:
- Wichtigkeit definiert durch das
!important
-Flag und Stilherkunft (Benutzer > Autor > Browser) - Spezifität der verwendeten Selektoren (Inline > ID > Klasse > Element)
- Quellreihenfolge des Codes selbst (neueste hat Vorrang)
Die Nähe wird nirgendwo erwähnt – die DOM-Baum-Beziehung zwischen Teilen eines Selektors. Die folgenden Absätze werden beide rot sein, obwohl #inner p
eine engere Beziehung beschreibt als #outer p
für den zweiten Absatz:
<section> <p>This text is red</p> <div> <p>This text is also red!</p> </div> </section>
#inner p { color: green; } #outer p { color: red; }
Beide Selektoren haben die gleiche Spezifität, sie beschreiben beide das gleiche p
-Element und keiner ist als !important
gekennzeichnet – das Ergebnis basiert also allein auf der Reihenfolge der Quellen.
BEM und Scoped Styles
Namenskonventionen wie BEM („Block__Element—Modifier“) werden verwendet, um sicherzustellen, dass jeder Absatz nur auf einen übergeordneten Abschnitt „beschränkt“ ist, wodurch die Kaskade vollständig vermieden wird. Absatz-„Elemente“ erhalten eindeutige Klassen, die für ihren „Block“-Kontext spezifisch sind:
<section class="outer"> <p class="outer__p">This text is red</p> <div class="inner"> <p class="inner__p">This text is green!</p> </div> </section>
.inner__p { color: green; } .outer__p { color: red; }
Diese Selektoren haben immer noch dieselbe relative Wichtigkeit, Spezifität und Quellenreihenfolge – aber die Ergebnisse sind unterschiedlich. „Scoped“- oder „modulare“ CSS-Tools automatisieren diesen Prozess, indem sie unser CSS für uns basierend auf dem HTML neu schreiben. Im folgenden Code bezieht sich jeder Absatz auf sein direktes übergeordnetes Element:
<section outer-scope> <p outer-scope>This text is red</p> <div outer-scope inner-scope> <p inner-scope>This text is green!</p> </div> </section>
p[inner-scope] { color: green } p[outer-scope] { color: red; }
Nachlass
Proximity ist kein Teil der Kaskade, aber es ist Teil von CSS. Das ist, wo Vererbung wichtig wird. Wenn wir das p
aus unseren Selektoren entfernen, erbt jeder Absatz eine Farbe von seinem nächsten Vorfahren:
#inner { color: green; } #outer { color: red; }
Da #inner
und #outer
unterschiedliche Elemente beschreiben, unser div
bzw. section
, werden beide Farbeigenschaften konfliktfrei angewendet. Für das verschachtelte p
-Element ist keine Farbe angegeben, sodass die Ergebnisse durch Vererbung (die Farbe des direkten übergeordneten Elements) und nicht durch cascade bestimmt werden. Nähe hat Vorrang, und der Wert #inner
überschreibt den Wert #outer
.
Aber es gibt ein Problem: Um die Vererbung zu verwenden, gestalten wir alles in unserem section
und div
. Wir wollen gezielt auf die Absatzfarbe abzielen.
(Wieder-)Einführung benutzerdefinierter Eigenschaften
Benutzerdefinierte Eigenschaften bieten eine neue, browsernative Lösung; Sie erben wie jede andere Eigenschaft, müssen aber nicht dort verwendet werden, wo sie definiert sind . Mit einfachem CSS, ohne Namenskonventionen oder Build-Tools, können wir einen Stil erstellen, der sowohl zielgerichtet als auch kontextbezogen ist, wobei die Nähe Vorrang vor der Kaskade hat:
p { color: var(--paragraph); } #inner { --paragraph: green; } #outer { --paragraph: red; }
Die benutzerdefinierte Eigenschaft --paragraph
erbt genauso wie die Eigenschaft color
, aber jetzt haben wir die Kontrolle darüber, wie und wo dieser Wert angewendet wird. Die Eigenschaft --paragraph
sich ähnlich wie ein Parameter, der an die p
-Komponente übergeben werden kann, entweder durch direkte Auswahl (Spezifitätsregeln) oder Kontext (Näherungsregeln).
Ich denke, dies offenbart ein Potenzial für benutzerdefinierte Eigenschaften, die wir oft mit Funktionen, Mixins oder Komponenten in Verbindung bringen.
Benutzerdefinierte „Funktionen“ und Parameter
Funktionen, Mixins und Komponenten basieren alle auf derselben Idee: wiederverwendbarer Code, der mit verschiedenen Eingabeparametern ausgeführt werden kann, um konsistente, aber konfigurierbare Ergebnisse zu erhalten. Der Unterschied besteht darin, was sie mit den Ergebnissen machen. Wir beginnen mit einer gestreiften Gradientenvariablen und können sie dann auf andere Formen erweitern:
html { --stripes: linear-gradient( to right, powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); }
Diese Variable ist im Root- html
-Element definiert (könnte auch :root
verwenden, aber das fügt unnötige Spezifität hinzu), sodass unsere Striped-Variable überall im Dokument verfügbar ist. Wir können es überall dort anwenden, wo Farbverläufe unterstützt werden:
body { background-image: var(--stripes); }
Parameter hinzufügen
Funktionen werden wie Variablen verwendet, definieren aber Parameter zur Änderung der Ausgabe. Wir können unsere Variable --stripes
so aktualisieren, dass sie funktionsähnlicher ist, indem wir einige parameterähnliche Variablen darin definieren. Ich beginne mit dem Ersetzen von to right
durch var(--stripes-angle)
, um einen Winkeländerungsparameter zu erstellen:
html { --stripes: linear-gradient( var(--stripes-angle), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); }
Es gibt andere Parameter, die wir erstellen könnten, je nachdem, welchem Zweck die Funktion dienen soll. Sollten wir Benutzern erlauben, ihre eigenen Streifenfarben auszuwählen? Wenn ja, akzeptiert unsere Funktion 5 verschiedene Farbparameter oder nur 3, die wie jetzt von außen nach innen gehen? Wollen wir auch Parameter für Farbstopps erstellen? Jeder Parameter, den wir hinzufügen, bietet mehr Anpassungsmöglichkeiten auf Kosten von Einfachheit und Konsistenz.
Es gibt keine universell richtige Antwort auf dieses Gleichgewicht – einige Funktionen müssen flexibler sein und andere müssen eigensinniger sein. Abstraktionen dienen dazu, Konsistenz und Lesbarkeit in Ihrem Code zu gewährleisten, also treten Sie einen Schritt zurück und fragen Sie nach Ihren Zielen. Was muss wirklich anpassbar sein und wo sollte Konsistenz erzwungen werden? In einigen Fällen kann es hilfreicher sein, zwei eigenständige Funktionen zu haben, anstatt eine vollständig anpassbare Funktion.
Um die obige Funktion zu verwenden, müssen wir einen Wert für den Parameter --stripes-angle
und die Ausgabe auf eine CSS-Ausgabeeigenschaft wie background-image
anwenden:
/* in addition to the code above… */ html { --stripes-angle: 75deg; background-image: var(--stripes); }
Vererbt versus universell
Aus Gewohnheit habe ich die Funktion --stripes
für das html
-Element definiert. Benutzerdefinierte Eigenschaften erben, und ich möchte, dass meine Funktion überall verfügbar ist, daher ist es sinnvoll, sie auf das Stammelement zu setzen. Das funktioniert gut zum Vererben von Variablen wie --brand-color: blue
, also könnten wir auch erwarten, dass es auch für unsere „Funktion“ funktioniert. Aber wenn wir versuchen, diese Funktion erneut für einen verschachtelten Selektor zu verwenden, funktioniert es nicht:
div { --stripes-angle: 90deg; background-image: var(--stripes); }
Der neue --stripes-angle
wird vollständig ignoriert. Es stellt sich heraus, dass wir uns bei Funktionen, die neu berechnet werden müssen, nicht auf die Vererbung verlassen können. Das liegt daran, dass jeder Eigenschaftswert einmal pro Element (in unserem Fall das html
-Stammelement) berechnet wird und der berechnete Wert dann vererbt wird . Indem wir unsere Funktion im Dokumentstamm definieren, stellen wir den Nachkommen nicht die gesamte Funktion zur Verfügung – nur das berechnete Ergebnis unserer Funktion.
Das macht Sinn, wenn Sie es in Bezug auf den kaskadierenden Parameter --stripes-angle
. Wie jede geerbte CSS-Eigenschaft ist sie für Nachkommen, aber nicht für Vorfahren verfügbar. Der Wert, den wir für ein verschachteltes div
festlegen, ist für eine Funktion, die wir für den html
-Root-Vorfahren definiert haben, nicht verfügbar. Um eine universell verfügbare Funktion zu erstellen, die für jedes Element neu berechnet wird, müssen wir sie für jedes Element definieren:
* { --stripes: linear-gradient( var(--stripes-angle), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); }
Der universelle Selektor macht unsere Funktion überall verfügbar, aber wir können sie enger definieren, wenn wir wollen. Wichtig ist, dass es nur dort nachrechnen kann, wo es explizit definiert ist. Hier sind ein paar alternativen:
/* make the function available to elements with a given selector */ .stripes { --stripes: /* etc… */; } /* make the function available to elements nested inside a given selector */ .stripes * { --stripes: /* etc… */; } /* make the function available to siblings following a given selector */ .stripes ~ * { --stripes: /* etc… */; }
Dies kann mit jeder Auswahllogik erweitert werden, die nicht auf Vererbung angewiesen ist.
Freie Parameter und Fallback-Werte
In unserem obigen Beispiel hat var(--stripes-angle)
keinen Wert und kein Fallback. Im Gegensatz zu Sass- oder JS-Variablen, die definiert oder instanziiert werden müssen, bevor sie aufgerufen werden, können benutzerdefinierte CSS-Eigenschaften aufgerufen werden, ohne jemals definiert zu werden. Dadurch entsteht eine „freie“ Variable, ähnlich einem Funktionsparameter, der aus dem Kontext geerbt werden kann.
Wir können die Variable schließlich in html
oder :root
(oder einem anderen Vorfahren) definieren, um einen geerbten Wert festzulegen, aber zuerst müssen wir den Fallback berücksichtigen, wenn kein Wert definiert ist. Es gibt mehrere Optionen, je nachdem, welches Verhalten wir genau wollen
- Für „erforderliche“ Parameter wollen wir keinen Fallback. So wie sie ist, wird die Funktion nichts tun, bis
--stripes-angle
definiert ist. - Für „optionale“ Parameter können wir einen Fallback-Wert in der Funktion
var()
bereitstellen. Nach dem Variablennamen fügen wir ein Komma hinzu, gefolgt vom Standardwert:
var(--stripes-angle, 90deg)
Jede var()
-Funktion kann nur einen Fallback haben – daher sind alle zusätzlichen Kommas Teil dieses Werts. Dadurch ist es möglich, komplexe Vorgaben mit internen Kommas zu versehen:
html { /* Computed: Hevetica, Ariel, sans-serif */ font-family: var(--sans-family, Hevetica, Ariel, sans-serif); /* Computed: 0 -1px 0 white, 0 1px 0 black */ test-shadow: var(--shadow, 0 -1px 0 white, 0 1px 0 black); }
Wir können auch verschachtelte Variablen verwenden, um unsere eigenen Kaskadenregeln zu erstellen und den verschiedenen Werten unterschiedliche Prioritäten zuzuweisen:
var(--stripes-angle, var(--global-default-angle, 90deg))
- Versuchen Sie zuerst unseren expliziten Parameter (
--stripes-angle
); - Fallback auf einen globalen „Benutzerstandard“ (
--user-default-angle
), falls verfügbar; - Fallback zum Schluss auf unsere „Werkseinstellung“
(90deg
).
Indem wir Fallback-Werte in var()
festlegen, anstatt die benutzerdefinierte Eigenschaft explizit zu definieren, stellen wir sicher, dass es keine Spezifitäts- oder Kaskadenbeschränkungen für den Parameter gibt. Alle *-angle
sind „frei“, um von jedem Kontext geerbt zu werden.
Browser-Fallbacks im Vergleich zu variablen Fallbacks
Wenn wir Variablen verwenden, müssen wir zwei Fallback-Pfade beachten:
- Welcher Wert sollte von Browsern ohne Variablenunterstützung verwendet werden?
- Welcher Wert sollte von Browsern verwendet werden, die Variablen unterstützen, wenn eine bestimmte Variable fehlt oder ungültig ist?
p { color: blue; color: var(--paragraph); }
Während alte Browser die Variablendeklarationseigenschaft ignorieren und auf blue
zurückgreifen, lesen moderne Browser beides und verwenden letzteres. Unser var(--paragraph)
ist möglicherweise nicht definiert, aber er ist gültig und überschreibt die vorherige Eigenschaft, sodass Browser mit Variablenunterstützung auf den geerbten oder anfänglichen Wert zurückgreifen, als ob sie das Schlüsselwort unset
verwenden würden.
Das mag zunächst verwirrend erscheinen, hat aber gute Gründe dafür. Der erste ist technischer Natur: Browser-Engines verarbeiten ungültige oder unbekannte Syntax zur „Parse-Zeit“ (was zuerst geschieht), aber Variablen werden erst zur „berechneten Wertzeit“ (was später passiert) aufgelöst.
- Zur Parse-Zeit werden Deklarationen mit ungültiger Syntax vollständig ignoriert und auf frühere Deklarationen zurückgegriffen. Dies ist der Weg, dem alte Browser folgen werden. Moderne Browser unterstützen die Variablensyntax, sodass die vorherige Deklaration stattdessen verworfen wird.
- Zum Zeitpunkt des berechneten Werts wird die Variable als ungültig kompiliert, aber es ist zu spät – die vorherige Deklaration wurde bereits verworfen. Gemäß der Spezifikation werden ungültige Variablenwerte genauso behandelt wie
unset
:
html { color: red; /* ignored as *invalid syntax* by all browsers */ /* - old browsers: red */ /* - new browsers: red */ color: not a valid color; color: var(not a valid variable name); /* ignored as *invalid syntax* by browsers without var support */ /* valid syntax, but invalid *values* in modern browsers */ /* - old browsers: red */ /* - new browsers: unset (black) */ --invalid-value: not a valid color value; color: var(--undefined-variable); color: var(--invalid-value); }
Dies ist auch gut für uns als Autoren, da wir mit komplexeren Fallbacks für die Browser spielen können, die Variablen unterstützen, und einfache Fallbacks für ältere Browser bereitstellen. Noch besser, das ermöglicht es uns, den null
/ undefined
Zustand zu verwenden, um die erforderlichen Parameter festzulegen. Dies wird besonders wichtig, wenn wir eine Funktion in ein Mixin oder eine Komponente umwandeln möchten.
Benutzerdefinierte Eigenschaft „Mixins“
In Sass geben die Funktionen Rohwerte zurück, während Mixins im Allgemeinen die tatsächliche CSS-Ausgabe mit Eigenschaft-Wert-Paaren zurückgeben. Wenn wir eine universelle Eigenschaft --stripes
definieren, ohne sie auf eine visuelle Ausgabe anzuwenden, ist das Ergebnis funktionsähnlich. Wir können dafür sorgen, dass sich das mehr wie ein Mixin verhält, indem wir die Ausgabe auch universell definieren:
* { --stripes: linear-gradient( var(--stripes-angle), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); background-image: var(--stripes); }
Solange --stripes-angle
ungültig oder undefiniert bleibt, kann das Mixin nicht kompiliert werden und es wird kein background-image
angewendet. Wenn wir für ein beliebiges Element einen gültigen Winkel festlegen, berechnet die Funktion und gibt uns einen Hintergrund:
div { --stripes-angle: 30deg; /* generates the background */ }
Leider wird dieser Parameterwert erben , sodass die aktuelle Definition einen Hintergrund für das div
und alle Nachkommen erstellt. Um das zu beheben, müssen wir sicherstellen, dass der Wert --stripes-angle
nicht erbt, indem wir ihn für jedes Element auf den initial
(oder einen ungültigen Wert) setzen. Wir können das mit demselben Universalselektor tun:
* { --stripes-angle: initial; --stripes: /* etc… */; background-image: var(--stripes); }
Sichere Inline-Stile
In einigen Fällen müssen wir den Parameter dynamisch von außerhalb von CSS festlegen – basierend auf Daten von einem Back-End-Server oder Front-End-Framework. Mit benutzerdefinierten Eigenschaften können wir Variablen sicher in unserem HTML definieren, ohne uns um die üblichen Spezifitätsprobleme kümmern zu müssen:
<div>...</div>
Inline-Stile haben eine hohe Spezifität und sind sehr schwer zu überschreiben – aber mit benutzerdefinierten Eigenschaften haben wir eine andere Option: ignorieren. Wenn wir das div auf background-image: none
(zum Beispiel) setzen, hat diese Inline-Variable keine Auswirkung. Um noch weiter zu gehen, können wir eine Zwischenvariable erstellen:
* { --stripes-angle: var(--stripes-angle-dynamic, initial); }
Jetzt haben wir die Möglichkeit, --stripes-angle-dynamic
im HTML zu definieren oder es zu ignorieren und --stripes-angle
direkt in unserem Stylesheet zu setzen.
Voreingestellte Werte
Für komplexere Werte oder gängige Muster, die wir wiederverwenden möchten, können wir auch einige voreingestellte Variablen zur Auswahl bereitstellen:
* { --tilt-down: 6deg; --tilt-up: -6deg; }
Und verwenden Sie diese Voreinstellungen, anstatt den Wert direkt festzulegen:
<div>...</div>
Dies ist großartig, um Diagramme und Grafiken basierend auf dynamischen Daten zu erstellen oder sogar einen Tagesplaner zu erstellen.
Kontextuelle Komponenten
Wir können unser „Mixin“ auch als „Komponente“ umgestalten, indem wir es auf einen expliziten Selektor anwenden und die Parameter optional machen. Anstatt sich auf das Vorhandensein oder Fehlen von --stripes-angle
zu verlassen, um unsere Ausgabe umzuschalten, können wir uns auf das Vorhandensein oder Fehlen eines Komponentenselektors verlassen. Dadurch können wir Fallback-Werte sicher festlegen:
[data-stripes] { --stripes: linear-gradient( var(--stripes-angle, to right), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); background-image: var(--stripes); }
Indem wir den Fallback in die var()
-Funktion einfügen, können wir --stripes-angle
undefiniert und „frei“ lassen, um einen Wert von außerhalb der Komponente zu erben. Dies ist eine großartige Möglichkeit, bestimmte Aspekte eines Komponentenstils für kontextbezogene Eingaben verfügbar zu machen. Sogar „bereichsbezogene“ Stile, die von einem JS-Framework generiert werden (oder innerhalb des Schatten-DOM, wie SVG-Symbole), können diesen Ansatz verwenden, um bestimmte Parameter für äußere Einflüsse verfügbar zu machen.
Isolierte Komponenten
Wenn wir den Parameter nicht für die Vererbung verfügbar machen möchten, können wir die Variable mit einem Standardwert definieren:
[data-stripes] { --stripes-angle: to right; --stripes: linear-gradient( var(--stripes-angle, to right), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); background-image: var(--stripes); }
Diese Komponenten würden auch mit einer Klasse oder einem anderen gültigen Selektor funktionieren, aber ich habe das data-
-Attribut gewählt, um einen Namensraum für alle gewünschten Modifikatoren zu erstellen:
[data-stripes='vertical'] { --stripes-angle: to bottom; } [data-stripes='horizontal'] { --stripes-angle: to right; } [data-stripes='corners'] { --stripes-angle: to bottom right; }
Selektoren und Parameter
Ich wünsche mir oft, ich könnte Datenattribute verwenden, um eine Variable zu setzen – eine Funktion, die von der CSS3 attr()
-Spezifikation unterstützt wird, aber noch in keinem Browser implementiert ist (siehe die Registerkarte Ressourcen für verknüpfte Probleme in jedem Browser). Das würde es uns ermöglichen, einen Selektor enger mit einem bestimmten Parameter zu verknüpfen:
<div data-stripes="30deg">...</div> /* Part of the CSS3 spec, but not yet supported */ /* attr( , ) */ [data-stripes] { --stripes-angle: attr(data-stripes angle, to right); }
<div data-stripes="30deg">...</div> /* Part of the CSS3 spec, but not yet supported */ /* attr( , ) */ [data-stripes] { --stripes-angle: attr(data-stripes angle, to right); }
<div data-stripes="30deg">...</div> /* Part of the CSS3 spec, but not yet supported */ /* attr( , ) */ [data-stripes] { --stripes-angle: attr(data-stripes angle, to right); }
In der Zwischenzeit können wir etwas Ähnliches erreichen, indem wir das Attribut style
verwenden:
<div>...</div> /* The `*=` atttribute selector will match a string anywhere in the attribute */ [style*='--stripes-angle'] { /* Only define the function where we want to call it */ --stripes: linear-gradient(…); }
Dieser Ansatz ist am nützlichsten, wenn wir zusätzlich zu dem festgelegten Parameter andere Eigenschaften einbeziehen möchten. Wenn Sie beispielsweise einen Rasterbereich festlegen, können Sie auch Polsterung und Hintergrund hinzufügen:
[style*='--grid-area'] { background-color: white; grid-area: var(--grid-area, auto / 1 / auto / -1); padding: 1em; }
Fazit
Wenn wir beginnen, all diese Teile zusammenzufügen, wird deutlich, dass benutzerdefinierte Eigenschaften weit über die üblichen Anwendungsfälle von Variablen hinausgehen, mit denen wir vertraut sind. Wir sind nicht nur in der Lage, Werte zu speichern und sie der Kaskade zuzuordnen – wir können sie auch verwenden, um die Kaskade auf neue Weise zu manipulieren und intelligentere Komponenten direkt in CSS zu erstellen.
Dies erfordert, dass wir viele der Tools überdenken, auf die wir uns in der Vergangenheit verlassen haben – von Namenskonventionen wie SMACSS und BEM bis hin zu „bereichsbezogenen“ Stilen und CSS-in-JS. Viele dieser Tools helfen dabei, Spezifitäten zu umgehen oder dynamische Stile in einer anderen Sprache zu verwalten – Anwendungsfälle, die wir jetzt direkt mit benutzerdefinierten Eigenschaften ansprechen können. Dynamische Stile, die wir oft in JS berechnet haben, können jetzt durch die Übergabe von Rohdaten an das CSS verarbeitet werden.
Zunächst mögen diese Änderungen als „zusätzliche Komplexität“ angesehen werden – da wir nicht daran gewöhnt sind, Logik in CSS zu sehen. Und wie bei jedem Code kann Over-Engineering eine echte Gefahr darstellen. Aber ich würde argumentieren, dass wir diese Macht in vielen Fällen nutzen können, um die Komplexität nicht zu erhöhen, sondern um die Komplexität aus den Tools und Konventionen von Drittanbietern zurück in die Kernsprache des Webdesigns und (was noch wichtiger ist) zurück in die Browser. Wenn unsere Stile eine Berechnung erfordern, sollte diese Berechnung in unserem CSS enthalten sein.
Alle diese Ideen können viel weiter getrieben werden. Benutzerdefinierte Eigenschaften werden gerade erst breiter angenommen, und wir haben gerade erst begonnen, an der Oberfläche dessen zu kratzen, was möglich ist. Ich bin gespannt, wohin das führt und was die Leute sich noch einfallen lassen. Habe Spaß!
Weiterführende Lektüre
- „Es ist an der Zeit, benutzerdefinierte CSS-Eigenschaften zu verwenden“, Serg Hospodarets
- „Ein Strategieleitfaden für benutzerdefinierte CSS-Eigenschaften“, Michael Riethmuller