BEM und moderne CSS-Selektoren im Griff

Avatar of Liam Johnston
Liam Johnston am

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

BEM. Wie scheinbar alle Techniken in der Welt der Front-End-Entwicklung kann das Schreiben von CSS im BEM-Format polarisieren. Aber es ist – zumindest in meiner Twitter-Bubble – eine der beliebteren CSS-Methoden.

Persönlich halte ich BEM für gut und denke, Sie sollten es verwenden. Aber ich verstehe auch, warum Sie es vielleicht nicht tun.

Unabhängig von Ihrer Meinung zu BEM bietet es mehrere Vorteile, der größte ist, dass es hilft, Spezifitätskollisionen in der CSS-Kaskade zu vermeiden. Das liegt daran, dass, wenn es richtig angewendet wird, alle Selektoren im BEM-Format denselben Spezifitätswert (0,1,0) haben sollten. Ich habe im Laufe der Jahre die CSS für viele große Websites entworfen (denken Sie an Regierungen, Universitäten und Banken), und gerade bei diesen größeren Projekten habe ich festgestellt, dass BEM wirklich glänzt. Das Schreiben von CSS macht viel mehr Spaß, wenn man sich darauf verlassen kann, dass die Styles, die man schreibt oder bearbeitet, nicht andere Teile der Website beeinflussen.

Es gibt tatsächlich Ausnahmen, bei denen es als völlig akzeptabel gilt, die Spezifität zu erhöhen. Zum Beispiel: die Pseudo-Klassen :hover und :focus. Diese haben einen Spezifitätswert von 0,2,0. Eine weitere sind Pseudo-Elemente – wie ::before und ::after –, die einen Spezifitätswert von 0,1,1 haben. Für den Rest dieses Artikels gehen wir jedoch davon aus, dass wir keine weitere Spezifitäts-Kriege wollen. 🤓

Aber ich bin nicht wirklich hier, um Sie von BEM zu überzeugen. Stattdessen möchte ich darüber sprechen, wie wir es zusammen mit modernen CSS-Selektoren – denken Sie an :is(), :has(), :where() usw. – verwenden können, um noch mehr Kontrolle über die Kaskade zu erhalten.

Worum geht es bei modernen CSS-Selektoren?

Die CSS Selectors Level 4 Spezifikation bietet uns einige mächtige, neue (nun ja, relativ neue) Wege, Elemente auszuwählen. Einige meiner Favoriten sind :is(), :where() und :not(), die alle von modernen Browsern unterstützt werden und heutzutage in fast jedem Projekt sicher verwendet werden können.

:is() und :where() sind im Grunde dasselbe, außer wie sie die Spezifität beeinflussen. Insbesondere hat :where() immer einen Spezifitätswert von 0,0,0. Ja, sogar :where(button#widget.some-class) hat keine Spezifität. Die Spezifität von :is() ist hingegen das Element in seiner Argumentliste mit der höchsten Spezifität. Wir haben also bereits eine Kaskaden-steuerende Unterscheidung zwischen zwei modernen Selektoren, mit der wir arbeiten können.

Die unglaublich mächtige relationale Pseudo-Klasse :has() gewinnt ebenfalls schnell Browser-Unterstützung (und ist meiner bescheidenen Meinung nach die größte neue Funktion von CSS seit Grid). Zum Zeitpunkt des Schreibens ist die Browserunterstützung für :has() jedoch noch nicht gut genug für den Produktionseinsatz.

Fügen wir eine dieser Pseudo-Klassen in meine BEM und...

/* ❌ specificity score: 0,2,0 */
.something:not(.something--special) {
  /* styles for all somethings, except for the special somethings */
}

Hoppla! Sehen Sie diesen Spezifitätswert? Denken Sie daran, dass wir mit BEM im Idealfall möchten, dass unsere Selektoren alle einen Spezifitätswert von 0,1,0 haben. Warum ist 0,2,0 schlecht? Betrachten Sie ein ähnliches Beispiel, erweitert

.something:not(a) {
  color: red;
}
.something--special {
  color: blue;
}

Auch wenn der zweite Selektor an letzter Stelle in der Quellreihenfolge steht, gewinnt die höhere Spezifität des ersten Selektors (0,1,1), und die Farbe von .something--special-Elementen wird auf red gesetzt. Vorausgesetzt, Ihr BEM ist richtig geschrieben und die ausgewählten Elemente haben sowohl die Basisklasse .something als auch die Modifier-Klasse .something--special im HTML angewendet.

Bei unvorsichtiger Verwendung können diese Pseudo-Klassen die Kaskade auf unerwartete Weise beeinflussen. Und es sind diese Inkonsistenzen, die später, insbesondere in größeren und komplexeren Codebasen, Kopfschmerzen bereiten können.

Mist. Und jetzt?

Erinnern Sie sich an das, was ich über :where() und die Tatsache gesagt habe, dass seine Spezifität null ist? Das können wir zu unserem Vorteil nutzen.

/* ✅ specificity score: 0,1,0 */
.something:where(:not(.something--special)) {
  /* etc. */
}

Der erste Teil dieses Selektors (.something) erhält seinen üblichen Spezifitätswert von 0,1,0. Aber :where() – und alles darin – hat eine Spezifität von 0, was die Spezifität des Selektors nicht weiter erhöht.

:where() ermöglicht uns das Verschachteln

Leute, die sich nicht so sehr um Spezifität kümmern wie ich (und das sind wahrscheinlich viele Leute, fairerweise), hatten bisher beim Verschachteln ziemlich leichtes Spiel. Mit ein paar sorglosen Tastenhieben können wir CSS wie dieses erhalten (beachten Sie, dass ich Sass zur Kürze verwende)

.card { ... }

.card--featured {
  /* etc. */  
  .card__title { ... }
  .card__title { ... }
}

.card__title { ... }
.card__img { ... }

In diesem Beispiel haben wir eine .card-Komponente. Wenn es sich um eine "Featured"-Karte handelt (unter Verwendung der Klasse .card--featured), müssen der Titel und das Bild der Karte anders gestylt werden. Aber, wie wir jetzt wissen, führt der obige Code zu einem Spezifitätswert, der inkonsistent mit dem Rest unseres Systems ist.

Ein Verfechter der Spezifität hätte stattdessen dies getan

.card { ... }
.card--featured { ... }
.card__title { ... }
.card__title--featured { ... }
.card__img { ... }
.card__img--featured { ... }

Das ist doch nicht so schlimm, oder? Ehrlich gesagt, das ist schönes CSS.

Es gibt jedoch einen Nachteil im HTML. Erfahrene BEM-Autoren sind sich wahrscheinlich schmerzlich der umständlichen Template-Logik bewusst, die erforderlich ist, um Modifier-Klassen bedingt auf mehrere Elemente anzuwenden. In diesem Beispiel muss die HTML-Vorlage bedingt die Modifier-Klasse --featured auf drei Elemente (.card, .card__title und .card__img) anwenden, obwohl in einem realen Beispiel wahrscheinlich noch mehr. Das sind viele if-Anweisungen.

Der :where()-Selektor kann uns helfen, deutlich weniger Template-Logik – und weniger BEM-Klassen – zu schreiben, ohne das Spezifitätsniveau zu erhöhen.

.card { ... }
.card--featured { ... }

.card__title { ... }
:where(.card--featured) .card__title { ... }

.card__img { ... }
:where(.card--featured) .card__img { ... }

Hier ist dasselbe, aber in Sass (beachten Sie die nachgestellten Ampersands)

.card { ... }
.card--featured { ... }
.card__title { 
  /* etc. */ 
  :where(.card--featured) & { ... }
}
.card__img { 
  /* etc. */ 
  :where(.card--featured) & { ... }
}

Ob Sie sich für diesen Ansatz gegenüber dem Anwenden von Modifier-Klassen auf die verschiedenen Kindelemente entscheiden sollten, ist eine Frage der persönlichen Vorliebe. Aber zumindest gibt uns :where() jetzt die Wahl!

Was ist mit Nicht-BEM-HTML?

Wir leben nicht in einer perfekten Welt. Manchmal müssen Sie mit HTML umgehen, das außerhalb Ihrer Kontrolle liegt. Zum Beispiel ein Drittanbieter-Skript, das HTML injiziert, das Sie gestalten müssen. Dieser Markup ist oft nicht mit BEM-Klassennamen geschrieben. In einigen Fällen verwenden diese Stile überhaupt keine Klassen, sondern IDs!

Auch hier hat :where() uns geholfen. Diese Lösung ist etwas hacky, da wir auf eine Klasse eines Elements verweisen müssen, das weiter oben im DOM-Baum existiert und von dem wir wissen, dass es existiert.

/* ❌ specificity score: 1,0,0 */
#widget {
  /* etc. */
}

/* ✅ specificity score: 0,1,0 */
.page-wrapper :where(#widget) {
  /* etc. */
}

Das Referenzieren eines übergeordneten Elements fühlt sich jedoch etwas riskant und einschränkend an. Was ist, wenn sich diese übergeordnete Klasse ändert oder aus irgendeinem Grund nicht vorhanden ist? Eine bessere (aber vielleicht ebenso hacky) Lösung wäre die Verwendung von :is(). Denken Sie daran, die Spezifität von :is() ist gleich der spezifischsten Selektor in seiner Selektorliste.

Anstatt also auf eine Klasse zu verweisen, die wir kennen (oder hoffen!), dass sie existiert, wie im obigen Beispiel, könnten wir auf eine erfundene Klasse und das <body>-Tag verweisen.

/* ✅ specificity score: 0,1,0 */
:is(.dummy-class, body) :where(#widget) {
  /* etc. */
}

Der allgegenwärtige body hilft uns, unser #widget-Element auszuwählen, und die Anwesenheit der Klasse .dummy-class innerhalb desselben :is() verleiht dem body-Selektor den gleichen Spezifitätswert wie eine Klasse (0,1,0)... und die Verwendung von :where() stellt sicher, dass der Selektor nicht spezifischer wird.

Das ist alles!

So können wir die modernen Spezifitätsverwaltungsfunktionen der Pseudo-Klassen :is() und :where() neben dem Spezifitätskollisionsschutz nutzen, den wir beim Schreiben von CSS im BEM-Format erhalten. Und in nicht allzu ferner Zukunft, sobald :has() die Firefox-Unterstützung erhält (es wird zum Zeitpunkt des Schreibens hinter einem Flag unterstützt), werden wir es wahrscheinlich mit :where() kombinieren wollen, um seine Spezifität aufzuheben.

Ob Sie sich nun voll und ganz auf die BEM-Namensgebung einlassen oder nicht, ich hoffe, wir können uns darauf einigen, dass Konsistenz bei der Selektorspezifität eine gute Sache ist!