Kämpfen Sie nicht gegen den Kaskadeneffekt, kontrollieren Sie ihn!

Avatar of Mads Stoumann
Mads Stoumann am

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

Wenn Sie diszipliniert sind und die Vererbung nutzen, die der CSS-Kaskadeneffekt bietet, werden Sie weniger CSS schreiben. Da unsere Stile jedoch oft aus allen möglichen Quellen stammen – und deren Strukturierung und Wartung mühsam sein kann –, kann der Kaskadeneffekt eine Quelle der Frustration sein und der Grund, warum wir am Ende mehr CSS als nötig haben.

Vor einigen Jahren entwickelte Harry Roberts ITCSS, eine clevere Methode zur Strukturierung von CSS.

In Verbindung mit BEM ist ITCSS zu einer beliebten Methode geworden, wie Menschen CSS schreiben und organisieren.

Selbst mit ITCSS und BEM kämpfen wir jedoch immer noch manchmal mit dem Kaskadeneffekt. Zum Beispiel sind Sie sicher schon einmal gezwungen gewesen, externe CSS-Komponenten an einer bestimmten Stelle mit @import zu importieren, um Fehler zu vermeiden, oder irgendwann zum gefürchteten !important gegriffen zu haben.

Vor kurzem wurden unserer CSS-Werkzeugkiste einige neue Werkzeuge hinzugefügt, die es uns endlich ermöglichen, den Kaskadeneffekt zu kontrollieren. Lassen Sie uns diese betrachten.

O Kaskadeneffekt, :where bist du?

Die Verwendung des :where Pseudoselektors ermöglicht es uns, die Spezifität auf „direkt nach den Standardstilen des Benutzeragenten“ zu reduzieren, unabhängig davon, wo oder wann das CSS in das Dokument geladen wird. Das bedeutet, dass die Spezifität des gesamten Dinges buchstäblich Null ist – komplett ausgelöscht. Dies ist praktisch für generische Komponenten, die wir uns gleich ansehen werden.

Stellen wir uns zunächst einige generische <table>-Stile mit :where vor

:where(table) {
  background-color: tan;
}

Wenn Sie nun vor dem :where-Selektor einige andere Tabellenstile hinzufügen, wie folgt

table {
  background-color: hotpink;
}

:where(table) {
  background-color: tan;
}

…wird der Hintergrund der Tabelle hotpink, obwohl der table-Selektor vor dem :where-Selektor im Kaskadeneffekt angegeben ist. Das ist die Schönheit von :where und warum es bereits für CSS-Resets verwendet wird.

:where hat einen Geschwisterteil, der fast die entgegengesetzte Wirkung hat: den :is-Selektor.

Die Spezifität der :is()-Pseudoklasse wird durch die Spezifität ihres spezifischsten Arguments ersetzt. Daher hat ein mit :is() geschriebener Selektor nicht unbedingt die gleiche Spezifität wie ein äquivalenter Selektor, der ohne :is() geschrieben wurde. Spezifikation von Selectors Level 4

Erweiterung unseres vorherigen Beispiels

:is(table) {
  --tbl-bgc: orange;
}
table {
  --tbl-bgc: tan;
}
:where(table) {
  --tbl-bgc: hotpink;
  background-color: var(--tbl-bgc);
}

Die Hintergrundfarbe von <table class="c-tbl"> wird tan sein, da die Spezifität von :is dieselbe wie die von table ist, aber table danach platziert ist.

Wenn wir es jedoch wie folgt ändern würden

:is(table, .c-tbl) {
  --tbl-bgc: orange;
}

…wird die Hintergrundfarbe orange sein, da :is das Gewicht seines schwersten Selektors hat, nämlich .c-tbl.

Beispiel: Eine konfigurierbare Tabellenkomponente

Nun wollen wir sehen, wie wir :where in unseren Komponenten verwenden können. Wir werden eine Tabellenkomponente erstellen, beginnend mit dem HTML

Wir umschließen .c-tbl mit einem :where-Selektor und fügen zur Freude abgerundete Ecken zur Tabelle hinzu. Das bedeutet, wir brauchen border-collapse: separate, da wir border-radius bei Tabellenzellen nicht verwenden können, wenn die Tabelle border-collapse: collapse verwendet.

:where(.c-tbl) {
  border-collapse: separate;
  border-spacing: 0;
  table-layout: auto;
  width: 99.9%;
}

Die Zellen verwenden unterschiedliche Stile für die <thead>- und <tbody>-Zellen

:where(.c-tbl thead th) {
  background-color: hsl(200, 60%, 40%);
  border-style: solid;
  border-block-start-width: 0;
  border-inline-end-width: 1px;
  border-block-end-width: 0;
  border-inline-start-width: 0;
  color: hsl(200, 60%, 99%);
  padding-block: 1.25ch;
  padding-inline: 2ch;
  text-transform: uppercase;
}
:where(.c-tbl tbody td) {
  background-color: #FFF;
  border-color: hsl(200, 60%, 80%);
  border-style: solid;
  border-block-start-width: 0;
  border-inline-end-width: 1px;
  border-block-end-width: 1px;
  border-inline-start-width: 0;
  padding-block: 1.25ch;
  padding-inline: 2ch;
}

Und wegen unserer abgerundeten Ecken und der fehlenden border-collapse: collapse müssen wir einige zusätzliche Stile hinzufügen, speziell für die Tabellenrahmen und einen Hover-Effekt auf den Zellen.

:where(.c-tbl tr td:first-of-type) {
  border-inline-start-width: 1px;
}
:where(.c-tbl tr th:last-of-type) {
  border-inline-color: hsl(200, 60%, 40%);
}
:where(.c-tbl tr th:first-of-type) {
  border-inline-start-color: hsl(200, 60%, 40%);
}
:where(.c-tbl thead th:first-of-type) {
  border-start-start-radius: 0.5rem;
}
:where(.c-tbl thead th:last-of-type) {
  border-start-end-radius: 0.5rem;
}
:where(.c-tbl tbody tr:last-of-type td:first-of-type) {
  border-end-start-radius: 0.5rem;
}
:where(.c-tbl tr:last-of-type td:last-of-type) {
  border-end-end-radius: 0.5rem;
}
/* hover */
@media (hover: hover) {
  :where(.c-tbl) tr:hover td {
    background-color: hsl(200, 60%, 95%);
  }
}

Jetzt können wir Variationen unserer Tabellenkomponente erstellen, indem wir andere Stile vor oder nach unseren generischen Stilen einfügen (dank der Spezifität-entfernenden Kräfte von :where), entweder durch Überschreiben des .c-tbl-Elements oder durch Hinzufügen einer BEM-ähnlichen Modifikator-Klasse (z.B. c-tbl--purple).

<table class="c-tbl c-tbl--purple">
.c-tbl--purple th {
  background-color: hsl(330, 50%, 40%)
}
.c-tbl--purple td {
  border-color: hsl(330, 40%, 80%);
}
.c-tbl--purple tr th:last-of-type {
  border-inline-color: hsl(330, 50%, 40%);
}
.c-tbl--purple tr th:first-of-type {
  border-inline-start-color: hsl(330, 50%, 40%);
}

Cool! Aber beachten Sie, wie wir ständig Farben wiederholen? Und was, wenn wir den border-radius oder die border-width ändern wollen? Das würde zu viel wiederholtem CSS führen.

Verschieben wir all diese in CSS Custom Properties und während wir dabei sind, können wir alle konfigurierbaren Eigenschaften an den Anfang des „Geltungsbereichs“ der Komponente – also des Tabellenelements selbst – verschieben, damit wir später leicht damit experimentieren können.

CSS Custom Properties

Ich werde das HTML ändern und ein data-component-Attribut auf dem Tabellenelement verwenden, das für das Styling angesprochen werden kann.

<table data-component="table" id="table">

Dieses data-component enthält die generischen Stile, die wir für jede Instanz der Komponente verwenden können, d.h. die Stile, die die Tabelle unabhängig von der angewendeten Farbvariante benötigt. Die Stile für eine spezifische Tabellenkomponenteninstanz werden in einer regulären Klasse enthalten sein, die Custom Properties der generischen Komponente verwendet.

[data-component="table"] {
  /* Styles needed for all table variations */
}
.c-tbl--purple {
  /* Styles for the purple variation */
}

Wenn wir alle generischen Stile in einem Data-Attribut platzieren, können wir jede beliebige Namenskonvention verwenden. So müssen wir uns keine Sorgen machen, wenn Ihr Chef darauf besteht, die Klassennamen der Tabelle wie .BIGCORP__TABLE, .table-component oder etwas anderes zu benennen.

In der generischen Komponente zeigt jede CSS-Eigenschaft auf eine Custom Property. Eigenschaften, die auf Kindelemente wirken müssen, wie border-color, werden auf dem Stamm der generischen Komponente festgelegt.

:where([data-component="table"]) {
  /* These will will be used multiple times, and in other selectors */
  --tbl-hue: 200;
  --tbl-sat: 50%;
  --tbl-bdc: hsl(var(--tbl-hue), var(--tbl-sat), 80%);
}

/* Here, it's used on a child-node: */
:where([data-component="table"] td) {
  border-color: var(--tbl-bdc);
}

Für andere Eigenschaften entscheiden Sie, ob sie einen statischen Wert haben soll oder mit ihrer eigenen Custom Property konfigurierbar sein soll. Wenn Sie Custom Properties verwenden, denken Sie daran, einen Standardwert zu definieren, auf den die Tabelle zurückfallen kann, falls eine Variantenklasse fehlt.

:where([data-component="table"]) {
  /* These are optional, with fallbacks */
  background-color: var(--tbl-bgc, transparent);
  border-collapse: var(--tbl-bdcl, separate);
}

Wenn Sie sich fragen, wie ich die Custom Properties benenne, verwende ich ein Komponentenpräfix (z. B. --tbl), gefolgt von einer Emmett-Abkürzung (z. B. -bgc). In diesem Fall ist --tbl das Komponentenpräfix, -bgc die Hintergrundfarbe und -bdcl die Rahmenkollaboration. Also, zum Beispiel, --tbl-bgc ist die Hintergrundfarbe der Tabellenkomponente. Ich verwende diese Namenskonvention nur bei Komponenten-Eigenschaften, im Gegensatz zu globalen Eigenschaften, die ich eher allgemeiner halte.

Wenn wir nun die Entwicklertools öffnen, können wir mit den Custom Properties experimentieren. Wir können zum Beispiel --tbl-hue in einen anderen Farbtonwert in der HSL-Farbe ändern, --tbl-bdrs: 0 setzen, um border-radius zu entfernen und so weiter.

A :where CSS rule set showing the custom properties of the table showing how the cascade’s specificity scan be used in context.

Wenn Sie mit Ihren eigenen Komponenten arbeiten, werden Sie an diesem Punkt feststellen, welche Parameter (d.h. die Werte der Custom Properties) die Komponente benötigt, um alles richtig aussehen zu lassen.

Wir können auch Custom Properties verwenden, um die Spaltenausrichtung und -breite zu steuern.

:where[data-component="table"] tr > *:nth-of-type(1)) {
  text-align: var(--ca1, initial);
  width: var(--cw1, initial);
  /* repeat for column 2 and 3, or use a SCSS-loop ... */
}

Wählen Sie in den Entwicklertools die Tabelle aus und fügen Sie diese dem element.styles-Selektor hinzu.

element.style {
  --ca2: center; /* Align second column center */
  --ca3: right; /* Align third column right */
}

Nun erstellen wir unsere spezifischen Komponenteneigenschaften mit einer regulären Klasse, .c-tbl (was im BEM-Jargon für „component-table“ steht). Wir fügen diese Klasse in das Tabellenmarkup ein.

<table class="c-tbl" data-component="table" id="table">

Ändern wir nun den --tbl-hue-Wert im CSS, nur um zu sehen, wie das funktioniert, bevor wir mit all den Eigenschaftswerten herumspielen.

.c-tbl {
  --tbl-hue: 330;
}

Beachten Sie, dass wir nur Eigenschaften aktualisieren müssen, anstatt komplett neue CSS zu schreiben! Die Änderung einer kleinen Eigenschaft aktualisiert die Farbe der Tabelle – keine neuen Klassen oder überschriebenen Eigenschaften weiter unten im Kaskadeneffekt.

Beachten Sie, wie sich auch die Rahmenfarben ändern. Das liegt daran, dass alle Farben in der Tabelle von der --tbl-hue-Variable erben.

Wir können einen komplexeren Selektor schreiben, aber trotzdem eine einzelne Eigenschaft aktualisieren, um so etwas wie Zebra-Streifen zu erhalten.

.c-tbl tr:nth-child(even) td {
  --tbl-td-bgc: hsl(var(--tbl-hue), var(--tbl-sat), 95%);
}

Und denken Sie daran: Es spielt keine Rolle, wo Sie die Klasse laden. Da unsere generischen Stile :where verwenden, wird die Spezifität aufgehoben, und alle benutzerdefinierten Stile für eine bestimmte Variante werden angewendet, egal wo sie verwendet werden. Das ist die Schönheit der Verwendung von :where zur Steuerung des Kaskadeneffekts!

Und das Beste daran: Wir können alle Arten von Tabellenkomponenten aus den generischen Stilen mit wenigen Zeilen CSS erstellen.

Lila Tabelle mit zebra-gestreiften Spalten
Helle Tabelle mit einem „noinlineborder“-Parameter… den wir als nächstes behandeln.

Hinzufügen von Parametern mit einem weiteren Datenattribut

Bisher alles gut! Die generische Tabellenkomponente ist sehr einfach. Aber was, wenn sie etwas benötigt, das eher echten Parametern ähnelt? Vielleicht für Dinge wie

  • zebra-gestreifte Zeilen und Spalten
  • ein fester Header und eine feste Spalte
  • Hover-Zustandsoptionen, wie Hover-Zeile, Hover-Zelle, Hover-Spalte

Wir könnten einfach BEM-ähnliche Modifikator-Klassen hinzufügen, aber wir können es tatsächlich effizienter erreichen, indem wir ein weiteres Datenattribut hinzufügen. Vielleicht ein data-param, das die Parameter wie folgt enthält:

<table data-component="table" data-param="zebrarow stickyrow">

Dann können wir in unserem CSS einen Attributselektor verwenden, um ein ganzes Wort in einer Liste von Parametern abzugleichen. Zum Beispiel zebra-gestreifte Zeilen.

[data-component="table"][data-param~="zebrarow"] tr:nth-child(even) td {
  --tbl-td-bgc: var(--tbl-zebra-bgc);
}

Oder zebra-gestreifte Spalten.

[data-component="table"][data-param~="zebracol"] td:nth-of-type(odd) {
  --tbl-td-bgc: var(--tbl-zebra-bgc);
}

Lassen Sie uns verrückt werden und sowohl den Tabellenkopf als auch die erste Spalte fixieren.


[data-component="table"][data-param~="stickycol"] thead tr th:first-child,[data-component="table"][data-param~="stickycol"] tbody tr td:first-child {
  --tbl-td-bgc: var(--tbl-zebra-bgc);
  inset-inline-start: 0;
  position: sticky;
}
[data-component="table"][data-param~="stickyrow"] thead th {
  inset-block-start: -1px;
  position: sticky;
}

Hier ist eine Demo, mit der Sie einen Parameter nach dem anderen ändern können.

Das standardmäßige helle Thema in der Demo ist dieses

.c-tbl--light {
  --tbl-bdrs: 0;
  --tbl-sat: 15%;
  --tbl-th-bgc: #eee;
  --tbl-th-bdc: #eee;
  --tbl-th-c: #555;
  --tbl-th-tt: normal;
}

…wobei data-param auf noinlineborder gesetzt ist, was diesen Stilen entspricht.

[data-param~="noinlineborder"] thead tr > th {
  border-block-start-width: 0;
  border-inline-end-width: 0;
  border-block-end-width: var(--tbl-bdw);
  border-inline-start-width: 0;
}

Ich weiß, dass meine data-attribute-Methode zum Stylen und Konfigurieren von generischen Komponenten sehr meinungsstark ist. So mache ich es eben, also fühlen Sie sich frei, bei jeder Methode zu bleiben, mit der Sie sich am wohlsten fühlen, sei es eine BEM-Modifikator-Klasse oder etwas anderes.

Die Quintessenz ist diese: Nutzen Sie :where und :is und die kaskadenkontrollierenden Kräfte, die sie bieten. Und, wenn möglich, konstruieren Sie das CSS so, dass Sie beim Erstellen neuer Komponentenvarianten so wenig neues CSS wie möglich schreiben.

Kaskadenebenen

Das letzte Werkzeug zur Kaskadenbewältigung, das ich mir ansehen möchte, sind „Cascade Layers“. Zum Zeitpunkt der Erstellung dieses Artikels handelt es sich um eine experimentelle Funktion, die in der Spezifikation von CSS Cascading and Inheritance Level 5 definiert ist und auf die Sie in Safari oder Chrome zugreifen können, indem Sie das Flag #enable-cascade-layers aktivieren.

Bramus Van Damme fasst das Konzept gut zusammen

Die wahre Stärke von Cascade Layers liegt in ihrer einzigartigen Position im Kaskadeneffekt: vor der Selektorspezifität und der Reihenfolge des Erscheinens. Deshalb müssen wir uns keine Sorgen um die Selektorspezifität des CSS machen, das in anderen Ebenen verwendet wird, noch um die Reihenfolge, in der wir CSS in diese Ebenen laden – etwas, das sich für größere Teams oder beim Laden von Drittanbieter-CSS als sehr nützlich erweisen wird.

Vielleicht noch schöner ist seine Illustration, die zeigt, wo Cascade Layers im Kaskadeneffekt liegen.

Quelle: Bramus Van Damme

Zu Beginn dieses Artikels erwähnte ich ITCSS – eine Methode zur Bändigung des Kaskadeneffekts durch Festlegung der Lade Reihenfolge von generischen Stilen, Komponenten usw. Cascade Layers ermöglichen es uns, ein Stylesheet an einer bestimmten Stelle einzufügen. Eine vereinfachte Version dieser Struktur in Cascade Layers sieht also so aus:

@layer generic, components;

Mit dieser einzigen Zeile haben wir die Reihenfolge unserer Ebenen festgelegt. Zuerst kommen die generischen Stile, gefolgt von den komponenten-spezifischen.

Nehmen wir an, wir laden unsere generischen Stile viel später als unsere Komponentenstile.

@layer components {
  body {
    background-color: lightseagreen;
  }
}

/* MUCH, much later... */

@layer generic { 
  body {
    background-color: tomato;
  }
}

Die background-color wird lightseagreen sein, da unsere Komponentenebenen-Stile nach den generischen Stilebenen festgelegt sind. Daher „gewinnen“ die Stile in der components-Ebene, auch wenn sie vor den generic-Ebenen-Stilen geschrieben sind.

Wieder einmal nur ein weiteres Werkzeug zur Steuerung der Anwendung von Stilen durch den CSS-Kaskadeneffekt, das uns mehr Flexibilität gibt, Dinge logisch zu organisieren, anstatt mit der Spezifität zu ringen.

Jetzt haben Sie die Kontrolle!

Der springende Punkt ist, dass der CSS-Kaskadeneffekt dank neuer Funktionen immer einfacher zu handhaben ist. Wir haben gesehen, wie die Pseudo-Selektoren :where und :is uns die Kontrolle über die Spezifität ermöglichen, entweder durch Entfernen der Spezifität eines gesamten Regelwerks oder durch Übernahme der Spezifität des spezifischsten Arguments. Dann haben wir CSS Custom Properties verwendet, um Stile zu überschreiben, ohne eine neue Klasse zum Überschreiben einer anderen zu schreiben. Von dort aus haben wir einen kleinen Abstecher in die Welt der Data-Attribute gemacht, um mehr Flexibilität zu schaffen, Komponentenvarianten zu erstellen, indem wir einfach Argumente zum HTML hinzufügen. Und schließlich haben wir uns Cascade Layers angesehen, die sich als nützlich erweisen werden, um die Lade Reihenfolge oder Stile mit @layer festzulegen.

Wenn Sie nur eine Erkenntnis aus diesem Artikel mitnehmen, hoffe ich, dass es ist, dass der CSS-Kaskadeneffekt nicht mehr der Feind ist, für den er oft gehalten wird. Wir erhalten die Werkzeuge, um aufzuhören, ihn zu bekämpfen und noch mehr darauf aufzubauen.


Header-Foto von Stephen Leonardi auf Unsplash