Ciao BEM, Servus Vanilla CSS

Der Abschied von BEM ist mir nicht leicht gefallen, doch es wird Zeit für was Neues. Und was Altes. Irgendwie.

Wie kam es eigentlich dazu, dass die BEM-Konvention zur Formulierung von CSS-Stylingklassen vor 15 Jahren so großen Erfolg hatte? Es gab mehrere valide Gründe:

  1. Vollständige Eliminierung des fehleranfälligen Konzepts der Spezifität, denn bei BEM haben alle CSS-Selektoren exakt die gleiche „Eine-Klasse-Spezifität“ von 0-1-0.
  2. Eine Art Styling-Kapselung voneinander getrennter Module durch je einen eindeutigen Namespace (der Block in Block-Element-Modifier)
  3. Unabhängigkeit des Stylings von Aufbau und Semantik des HTML

Doch es hat sich inzwischen ein bisschen überholt, das ganze. Auch dafür gibt es mehrere Gründe. Diesmal in Prosa statt als Liste. Zunächst mal nervt es ungemein, dass bei strenger BEM-Anwendung die Modifikations-Klassen zusätzlich zu den regulären Element-Klassen vergeben werden muss. Liest sich dann so:

<div class="multipass">
    <h3 class="multipass__headline multipass__headline--active">Leeloo</h3>
</div>

Sieht erstmal scheiße aus. Und wenn man den active-Zustand per JavaScript dynamisch setzen möchte, macht es das nicht hübscher. Modifikatoren sollten eher kompakte Zusatz-Klassen sein, aber dann brechen mehrere eiserne BEM-Konzepte.

BEM war auf CSS-Seite eigentlich nur in Kombination mit Sass und Nesting irgendwie erträglich. Hier konnte man die ganze Geschwätzigkeit des HTML-Codes wegabstrahieren:

.multipass {
    background: #ccc;

    &__headline {
        font-size: 2rem;

        &--active {
            color: #c00;
        }
    }
}

Da Sass ebenfalls nicht mehr cool ist (darüber gibt es bald einen gesonderten Artikel), könnte man das noch mit nativem CSS-Nesting probieren. Habe ich probiert – geht aber nicht! Denn obwohl der &-Platzhalter existiert, kann er in nativem CSS nicht mit Teil-Strings kombiniert werden. Ein Grund mehr, wieder vernünftige Klassen zu verwenden und das CSS wieder etwas stärker an die Struktur des HTML zu binden. (In der Praxis hat sich das mit der absoluten Trennung der beiden Strukturen eh nie ganz eingelöst).

<div class="multipass">
    <h3 class="headline active">Leeloo</h3>
</div>

und im CSS dazu so:

.multipass {
    background: #ccc;

    .headline {
        font-size: 2rem;

        &.active {
            color: #c00;
        }
    }
}

Ein Hauch von 2006, mit einer Prise Modulkapselung, optisch angehübscht durch übersichtliches Nesting. Aber wir wollen mehr. Und zwar vor allem: die coolen modernen Selektoren nutzen, die für orthodoxe BEM-Gläubige tabu sind. Dinge, die ich seither sehr gerne nutze, weil ich es mir zur Aufgabe gemacht habe, möglichst sparsam Klassen zu verwenden, da diese – sehen wir es ein – keine echte Semantik in sich tragen. Statt für jeden Scheiß eine Klasse oder einen geschwätzigen BEM-Modifier herbeizufantasieren, kann man auch folgende Freunde einsetzen:

Die Verwandschafts-Selektoren

.multipass + .multipass { /* Elemente direkt folgend */ }
.multipass ~ .multipass { /* Elemente nachfolgend mit Zeug dazwischen */ }
.multipass > .headline { /* Kind-Selektor */ }
.multipass > &:nth-child(2) { /* bestimmes Kind-Selektor }
[…]

Ich meine, wir hatten diese Schätze schon fast wieder vergessen; Das ist alles Standardzeug von CSS3. Gibt es seit hundert Jahren, und ist kein bisschen weniger cool geworden. Man darf halt nicht ständig sein HTML umstrukturieren, das ist der Nachteil, zugegebenerweise.

Die Aria-Selektoren

Wann immer ein Status sowieso per aria-Attribut ausgewiesen werden sollte, kann man den auch gleich als CSS-Hook mitverwenden und spart sich somit die Extra-Klasse im HTML:

<menu class="menulist">
    <li class="item">
        <a class="link" aria-current="page" href="element5.html">Leeloo</a></li>
    </li>
</menu>
.menulist {
    .item {}
    .link { 
        color: black;

        &[aria-current='page'],
        &:hover {
            background: black;
            color: white;
        }
    }
}

Die coolen Selektoren von heute

Großer Hype vor 3 Jahren, wahrscheinlich berechtigt. Ich beginne tatsächlich erst jetzt, ganz zaghaft (zu lange im BEM-Land verharrt), die Vorzüge von :has und :not zu schätzen:

.menulist {
    .item {
        background: blue;
        &:has([aria-current]) {
            box-shadow: 0 0 10px rgba(0,0,0,0.3);
        }
        &:not(:first-child) {
            font-size: 1.2rem;
        }
    }
}

Ihr merkt schon, ein paar Dinge sind geblieben aus der Lehre der letzten 15 Jahre:

  • Nach Möglichkeit keine Element-Selektoren wie h2 verwenden – das fesselt uns dann doch zu sehr an die Semantik des HTML.
  • Aufteilung des Layouts in Blöcke bzw. Module mit einzigartigem Namespace, um voneinander getrennte Layout-Bestandteile auch getrennt zu stylen.
  • Innerhalb eines Layout-Moduls möglichst flache Hierarchie, so dass die meisten Selektoren eine Spezifität von 0-2-0 haben.

Und weil es alles natürlich nicht ganz so einfach ist, wenn die Websites komplexer werden, gibt es noch ein weiteres Konzept, welches ich zu schätzen gelernt habe:

Die neue @layer Funktion

Wenn unsere Website ausschließlich aus gekapselten Layout-Modulen bestehen würde, wäre das jetzt gar nicht notwendig, aber es gibt meist noch ein paar andere Ebenen. Und auch da sollten sich möglichst wenig Styling-Zuständigkeiten in die Quere kommen, beziehungsweise sollte die Kaskade sauber dafür sorgen, dass spezifischere Gestaltungen die allgemeineren Gestaltungen überschreiben.

In der Praxis sieht das so aus, dass ich meine CSS-Regeln in 6 Schichten aufteile:

<style>
    @layer reset, base, usertext, vendor, utilities, components;
</style>

Ich gehe das mal mit euch durch:

  1. reset ist das Reset-Stylesheet. Ob man das 2025 noch macht, weiß ich gar nicht. Ich mache es noch.
  2. base enthält sowohl meine CSS-Variablen-Deklarationen, als auch grundlegendes Seitenlayout, also das Styling für body, main usw. – und natürlich auch die @font-face-Deklarationen.
  3. usertext ist der Layer für strukturierten Text, der von Redakteuren im CMS verfasst wird. Er enthält keine Klassen und ist der einzige Ort, wo Element-Selektoren erlaubt sind.
  4. vendor enthält Fremd-CSS von Slideshows, Lightboxen, Cookiebannern und ähnlichen Modulen.
  5. utilities sind die berühmten Hilfsklassen, die überall verwendet werden können, innerhalb und außerhalb von allen Layout-Kapseln. Darunter auch Formular-Elemente wie .button oder .textinput – oder .wrap für die zentrierte Maximalbreite, oder .hidden zum Verstecken. Ich übertreibe es nicht mit der Vielzahl an Utilities, wir sind ja hier nicht bei Tailwind.
  6. components sind dann endlich unsere Layout-Module, die wir von oben kennen, und die früher unsere BEM-Blöcke waren.

Der Clou ist, dass die Reihenfolge der Layer sehr wichtig ist: Wenn ich ein HTML-Element habe, welches von mehreren Selektoren in verschiedenen Layern seine Styles abbekommt, dann gewinnt im Konfliktfall immer das spätere Layer. Egal, wie hoch die jeweilige Spezifität innerhalb des Layers ist.

Ich könnte also im Extremfall im reset-Layer alle margins aller Element mit !important auf 0 festzurren – über utitilies könnte ich den margin ganz easy trotzdem überschreiben, selbst mit einem schwachen Element-Selektor.

Wir beginnen also mit den unspezifischsten Regeln in Reset und arbeiten uns zu den spezifischsten Regeln in den Komponenten hoch. Dies ist übrigens für jeden CSS-Autor frei wählbar, es ist nur meine Methode – möglicherweise machen andere Autoren eine ganz andere Aufteilung.

Styling-Konflikte …

… gibt es natürlich immer noch. Insbesondere, wenn man etwas längere Pausen in der Entwicklung eines Projekts macht und nicht mehr genau im Kopf hat, ob man beispielsweise die Klasse .figure bereits als Utility angelegt hat und sie jetzt auch noch als Komponente verwenden möchte. Das bekommt man aber schnell raus. Im Zweifelsfall wählt man den Klassennamen der Komponente etwas länger und spezifischer, etwa .herofigure.


So, das war’s für heute. Im nächsten Artikel zeige ich euch, wie ich die ganzen CSS-Dateien im Projekt organisiere, und wie ich ohne Building-Prozess einen guten Kompromiss aus Performance und Wartbarkeit erreiche.