CSS Custom Properties aus :root herauszulösen, mag eine gute Idee sein

Avatar of Kevin Powell
Kevin Powell am

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

CSS Custom Properties sind seit einiger Zeit ein heißes Thema, mit unzähligen großartigen Artikeln darüber, von großartigen Einführungen in ihre Funktionsweise über kreative Tutorials bis hin zu echter Magie mit ihnen. Wenn Sie mehr als ein oder zwei Artikel zu diesem Thema gelesen haben, sind Sie sicher schon darauf gestoßen, dass sie zu etwa 99 % damit beginnen, die benutzerdefinierten Eigenschaften auf :root zu setzen.

Während es großartig ist, benutzerdefinierte Eigenschaften auf :root für Dinge zu setzen, die auf Ihrer gesamten Website verfügbar sein müssen, gibt es Zeiten, in denen es sinnvoller ist, Ihre benutzerdefinierten Eigenschaften lokal zu begrenzen.

In diesem Artikel werden wir untersuchen:

  • Warum wir benutzerdefinierte Eigenschaften überhaupt auf :root setzen.
  • Warum die globale Abgrenzung nicht für alles richtig ist.
  • Wie man Klassenkonflikte mit lokal abgegrenzten benutzerdefinierten Eigenschaften überwindet.

Was ist es mit Custom Properties und :root?

Bevor wir uns mit dem globalen Geltungsbereich befassen, denke ich, es lohnt sich zu betrachten, warum jeder benutzerdefinierte Eigenschaften überhaupt in :root setzt.

Ich habe benutzerdefinierte Eigenschaften auf :root deklariert, ohne überhaupt nachzudenken. Praktisch jeder tut es, ohne es überhaupt zu erwähnen – einschließlich der offiziellen Spezifikation.

Wenn das Thema :root tatsächlich angesprochen wird, wird erwähnt, wie :root dasselbe wie html ist, aber mit höherer Spezifität, und das ist alles.

Aber spielt diese höhere Spezifität wirklich eine Rolle?

Nicht wirklich. Alles, was es tut, ist, html mit höherer Spezifität auszuwählen, genauso wie ein Klassenselektor eine höhere Spezifität hat als ein Elementselektor bei der Auswahl eines div.

:root {
  --color: red;
}

html {
  --color: blue;
}

.example {
  background: var(--color);
  /* Will be red because of :root's higher specificity */
}

Der Hauptgrund, warum :root vorgeschlagen wird, ist, dass CSS nicht nur zur Gestaltung von HTML-Dokumenten verwendet wird. Es wird auch für XML- und SVG-Dateien verwendet.

Im Fall von XML- und SVG-Dateien wählt :root nicht das html-Element aus, sondern seine Wurzel (wie das svg-Tag in einer SVG-Datei).

Aus diesem Grund ist die beste Praxis für eine global abgegrenzte benutzerdefinierte Eigenschaft :root. Aber wenn Sie eine Website erstellen, können Sie sie auf einen html-Selektor anwenden und keinen Unterschied bemerken.

Das gesagt, mit *jeder* der :root verwendet, ist es schnell zu einem "Standard" geworden. Es hilft auch, Variablen, die später verwendet werden sollen, von Selektoren zu trennen, die das Dokument aktiv gestalten.

Warum der globale Geltungsbereich nicht für alles richtig ist

Mit CSS-Präprozessoren wie Sass und Less bewahren die meisten von uns Variablen in einer dafür vorgesehenen Teil-Datei auf. Das funktioniert gut, warum sollten wir also plötzlich in Erwägung ziehen, Variablen lokal abzugrenzen?

Ein Grund ist, dass manche Leute vielleicht so etwas tun.

:root {
  --clr-light: #ededed;
  --clr-dark: #333;
  --clr-accent: #EFF;
  --ff-heading: 'Roboto', sans-serif;
  --ff-body: 'Merriweather', serif;
  --fw-heading: 700;
  --fw-body: 300;
  --fs-h1: 5rem;
  --fs-h2: 3.25rem;
  --fs-h3: 2.75rem;
  --fs-h4: 1.75rem;
  --fs-body: 1.125rem;
  --line-height: 1.55;
  --font-color: var(--clr-light);
  --navbar-bg-color: var(--clr-dark);
  --navbar-logo-color: var(--clr-accent);
  --navbar-border: thin var(--clr-accent) solid;
  --navbar-font-size: .8rem;
  --header-color: var(--clr-accent);
  --header-shadow: 2px 3px 4px rgba(200,200,0,.25);
  --pullquote-border: 5px solid var(--clr-light);
  --link-fg: var(--clr-dark);
  --link-bg: var(--clr-light);
  --link-fg-hover: var(--clr-dark);
  --link-bg-hover: var(--clr-accent);
  --transition: 250ms ease-out;
  --shadow: 2px 5px 20px rgba(0, 0, 0, .2);
  --gradient: linear-gradient(60deg, red, green, blue, yellow);
  --button-small: .75rem;
  --button-default: 1rem;
  --button-large: 1.5rem;
}

Sicher, das gibt uns einen Ort, an dem wir die Gestaltung mit benutzerdefinierten Eigenschaften verwalten können. Aber warum müssen wir meine --header-color oder --header-shadow in meinem :root definieren? Dies sind keine globalen Eigenschaften, ich verwende sie eindeutig in meiner Kopfzeile und nirgendwo sonst.

Wenn es keine globale Eigenschaft ist, warum sie dann global definieren? Dort kommt die lokale Abgrenzung ins Spiel.

Lokal abgegrenzte Eigenschaften in Aktion

Nehmen wir an, wir haben eine Liste zu gestalten, aber unsere Website verwendet ein Icon-System – nehmen wir der Einfachheit halber Font Awesome. Wir wollen für die Aufzählungspunkte unserer ul keine disc verwenden – wir wollen ein benutzerdefiniertes Icon!

Wenn ich die Aufzählungszeichen einer unsortierten Liste durch Font Awesome-Icons ersetzen möchte, können wir etwas wie folgt tun:

ul {
  list-style: none;
}

li::before {
  content: "\f14a"; /* checkbox */
  font-family: "Font Awesome Free 5";
  font-weight: 900;
  float: left;
  margin-left: -1.5em;
}

Das ist zwar super einfach zu machen, aber eines der Probleme ist, dass das Icon abstrakt wird. Wenn wir Font Awesome *nicht sehr oft* verwenden, werden wir nicht wissen, was f14a bedeutet, geschweige denn es als Checkbox-Icon identifizieren können. Es ist semantisch bedeutungslos.

Wir können hier mit einer benutzerdefinierten Eigenschaft helfen, die Dinge zu verdeutlichen.

ul {
  --checkbox-icon: "\f14a";
  list-style: none;
}

Das wird deutlich praktikabler, wenn wir ein paar verschiedene Icons im Einsatz haben. Erhöhen wir die Komplexität und sagen wir, wir haben drei verschiedene Listen:

<ul class="icon-list checkbox-list"> ... </ul>

<ul class="icon-list star-list"> ... </ul>

<ul class="icon-list bolt-list"> ... </ul>

Dann können wir in unserem CSS die benutzerdefinierten Eigenschaften für unsere verschiedenen Icons erstellen:

.icon-list {
  --checkbox: "\f14a";
  --star: "\f005";
  --bolt: "\f0e7";

  list-style: none;
}

Die wahre Stärke von lokal abgegrenzten benutzerdefinierten Eigenschaften zeigt sich, wenn wir die Icons tatsächlich anwenden wollen.

Wir können content: var(--icon) auf unsere Listenelemente setzen:

.icon-list li::before {
  content: var(--icon);
  font-family: "Font Awesome Free 5";
  font-weight: 900;
  float: left;
  margin-left: -1.5em;
}

Dann können wir dieses Icon für jede unserer Listen mit aussagekräftigeren Namen definieren:

.checkbox-list {
  --icon: var(--checkbox);
}

.star-list {
  --icon: var(--star);
}

.bolt-list {
  --icon: var(--bolt);
}

Wir können dies noch einen Schritt weiter treiben, indem wir Farben hinzufügen:

.icon-list li::before {
  content: var(--icon);
  color: var(--icon-color);
  /* Other styles */
}

Icons in den globalen Geltungsbereich verschieben

Wenn wir mit einem Icon-System wie Font Awesome arbeiten, gehe ich davon aus, dass wir sie für mehr als nur den Ersatz von Aufzählungszeichen in unsortierten Listen verwenden werden. Solange wir sie an mehr als einer Stelle verwenden, macht es Sinn, die Icons nach :root zu verschieben, da wir möchten, dass sie global verfügbar sind.

Icons in :root zu haben bedeutet nicht, dass wir nicht trotzdem von lokal abgegrenzten benutzerdefinierten Eigenschaften profitieren können!

:root {
  --checkbox: "\f14a";
  --star: "\f005";
  --bolt: "\f0e7";
  
  --clr-success: rgb(64, 209, 91);
  --clr-error: rgb(219, 138, 52);
  --clr-warning: rgb(206, 41, 26);
}

.icon-list li::before {
  content: var(--icon);
  color: var(--icon-color);
  /* Other styles */
}

.checkbox-list {
  --icon: var(--checkbox);
  --icon-color: var(--clr-success);
}

.star-list {
  --icon: var(--star);
  --icon-color: var(--clr-warning);
}

.bolt-list {
  --icon: var(--bolt);
  --icon-color: var(--clr-error);
}

Fallbacks hinzufügen

Wir könnten entweder ein Standard-Icon einfügen, indem wir es als Fallback festlegen (z. B. var(--icon, "/f1cb")), oder, da wir die content-Eigenschaft verwenden, könnten wir sogar eine Fehlermeldung einfügen: var(--icon, "no icon set").

Sehen Sie den Stift
Benuzerdefinierte Listen-Icons mit CSS Custom Properties
von Kevin (@kevinpowell)
auf CodePen.

Indem wir die Variablen --icon und --icon-color lokal abgrenzen, haben wir die Lesbarkeit unseres Codes erheblich verbessert. Wenn jemand Neues in das Projekt kommt, wird es ihm wesentlich leichter fallen zu verstehen, wie es funktioniert.

Das ist natürlich nicht auf Font Awesome beschränkt. Lokal abgegrenzte benutzerdefinierte Eigenschaften eignen sich auch hervorragend für ein SVG-Icon-System.

:root {
  --checkbox: url(../assets/img/checkbox.svg);
  --star: url(../assets/img/star.svg);
  --baby: url(../assets/img/baby.svg);
}

.icon-list {
  list-style-image: var(--icon);
}

.checkbox-list { --icon: checkbox; }
.star-list { --icon: star; }
.baby-list { --icon: baby; }

Verwendung lokal abgegrenzter Eigenschaften für modulareren Code

Während das gerade betrachtete Beispiel gut dazu dient, die Lesbarkeit unseres Codes zu erhöhen – was großartig ist –, können wir mit lokal abgegrenzten Eigenschaften noch viel mehr erreichen.

Manche Leute lieben CSS, wie es ist; andere hassen es, mit dem globalen Geltungsbereich der Kaskade zu arbeiten. Ich bin nicht hier, um CSS-in-JS zu diskutieren (es gibt genug sehr kluge Leute, die bereits darüber reden), aber lokal abgegrenzte benutzerdefinierte Eigenschaften bieten uns einen fantastischen Mittelweg.

Durch die Nutzung lokal abgegrenzter benutzerdefinierter Eigenschaften können wir sehr modularen Code erstellen, der einen Großteil des Aufwands bei der Suche nach aussagekräftigen Klassennamen erspart.

Lassen Sie uns, äh, das Szenario *abgrenzen*.

Ein Teil des Grundes, warum Leute von CSS frustriert sind, ist, dass die folgende Markup Probleme verursachen kann, wenn wir etwas gestalten wollen.

<div class="card">
  <h2 class="title">This is a card</h2>
  <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Libero, totam.</p>
  <button class="button">More info</button>
</div>

<div class="cta">
  <h2 class="title">This is a call to action</h2>
  <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aliquid eveniet fugiat ratione repellendus ex optio, ipsum modi praesentium, saepe, quibusdam rem quaerat! Accusamus, saepe beatae!</p>
  <button class="button">Buy now</button>
</div>

Wenn ich einen Stil für die Klasse .title erstelle, wird er *beide* Elemente stylen, die die Klassen .card und .cta enthalten. Wir können einen zusammengesetzten Selektor verwenden (d. h. .card .title), aber das erhöht die Spezifität, was zu geringerer Wartbarkeit führen kann. Oder wir können einen BEM-Ansatz verfolgen und unsere Klasse .title in .card__title und .cta__title umbenennen, um diese Elemente etwas mehr zu isolieren.

Lokal abgegrenzte benutzerdefinierte Eigenschaften bieten uns jedoch eine großartige Lösung. Wir können sie auf die Elemente anwenden, wo sie verwendet werden:

.title {
  color: var(--title-clr);
  font-size: var(--title-fs);
}

.button {
  background: var(--button-bg);
  border: var(--button-border);
  color: var(--button-text);
}

Dann können wir alles, was wir brauchen, innerhalb ihrer übergeordneten Selektoren steuern:

.card {
  --title-clr: #345;
  --title-fs: 1.25rem;
  --button-border: 0;
  --button-bg: #333;
  --button-text: white;
}

.cta {
  --title-clr: #f30;
  --title-fs: 2.5rem;
  --button-border: 0;
  --button-bg: #333;
  --button-text: white;
}

Höchstwahrscheinlich gibt es einige Standardwerte oder Gemeinsamkeiten zwischen Schaltflächen oder Titeln, auch wenn sie sich in verschiedenen Komponenten befinden. Dafür könnten wir Fallbacks einbauen oder sie einfach wie gewohnt gestalten.

.button {
  /* Custom variables with default values */
  border: var(--button-border, 0);    /* Default: 0 */
  background: var(--button-bg, #333); /* Default: #333 */
  color: var(--button-text, white);   /* Default: white */

  /* Common styles every button will have */
  padding: .5em 1.25em;
  text-transform: uppercase;
  letter-spacing: 1px;
}

Wir könnten sogar calc() verwenden, um unserer Schaltfläche eine Skalierung hinzuzufügen, was das Potenzial hat, die Notwendigkeit von Klassen wie .btn-sm und .btn-lg zu beseitigen (oder es könnte je nach Situation in diese Klassen integriert werden).

.button {
  font-size: calc(var(--button-scale) * 1rem);
  /* Multiply `--button-scale` by `1rem` to add unit */
}

.cta {
  --button-scale: 1.5;
}

Hier ist ein detaillierterer Blick auf all dies in Aktion:

Sehen Sie den Stift
Benuzerdefinierte Listen-Icons mit CSS Custom Properties
von Kevin (@kevinpowell)
auf CodePen.

Beachten Sie im obigen Beispiel, dass ich einige generische Klassen verwendet habe, wie .title und .button, die mit lokal abgegrenzten Eigenschaften (mit Hilfe von Fallbacks) gestaltet sind. Da diese mit benutzerdefinierten Eigenschaften eingerichtet sind, kann ich sie lokal innerhalb des übergeordneten Selektors definieren und ihnen so effektiv ihren eigenen Stil geben, ohne dass ein zusätzlicher Selektor erforderlich ist.

Ich habe auch einige Preis-Karten mit Modifikatorklassen eingerichtet. Mit der generischen Klasse .pricing habe ich alles eingerichtet und dann mit Modifikatorklassen einige Eigenschaften wie --text und --background neu definiert, ohne mich um zusammengesetzte Selektoren oder zusätzliche Klassen kümmern zu müssen.

Wenn man auf diese Weise arbeitet, führt dies zu sehr wartbarem Code. Es ist einfach, die Farbe einer Eigenschaft zu ändern, wenn wir das müssen, oder sogar ein völlig neues Thema oder einen neuen Stil zu erstellen, wie die Regenbogen-Variante der Preis-Karte im Beispiel.

Es erfordert ein wenig Voraussicht bei der anfänglichen Einrichtung, aber die Belohnung kann großartig sein. Es mag sogar kontraintuitiv erscheinen zu der Art, wie Sie es gewohnt sind, Stile anzugehen, aber wenn Sie das nächste Mal eine benutzerdefinierte Eigenschaft erstellen, versuchen Sie, sie lokal zu definieren, wenn sie nicht global leben muss, und Sie werden sehen, wie nützlich sie sein kann.