Wird das CSS Scope Feature die View Encapsulation von Angular ersetzen?

Entwickler:innen sind oft bequem. Wir lieben Tools und Features, die uns das Leben leichter machen. Das gilt insbesondere für die Gestaltung von Webinhalten mit CSS.

Eine große Herausforderung ist die Definition von CSS-Regeln, die nur bestimmte Bereiche stylen, ohne allzu spezifische Selektoren zu schreiben, die nur schwer zu überschreiben sind. Außerdem sollte man Selektoren nicht zu eng an die DOM-Struktur koppeln, da diese anfällig für Änderungen ist.

Verschiedene JavaScript-Frameworks haben unterschiedliche Lösungen für dieses Problem entwickelt: React verwendet CSS-Module, die das Scoping von CSS ermöglichen, indem sie automatisch einen eindeutigen Klassennamen für Komponenten-Styles erstellen. In Angular werden die Styles mit benutzerdefinierten HTML-Attributen gekapselt, so dass sie sich nicht auf den Rest der Applikation auswirken.

Eine Schachtel mit sechs Donuts in verschiedenen Farben. Foto: © cottonbro studio / pexels.com

Aber warum ist das Scoping von Styles nicht allein mit CSS möglich? Mittlerweile geht das! Mithilfe der neuen CSS-Regel @scope können wir Styles auf bestimmte DOM-Abschnitte beschränken. Ihr könnt sogar untere Grenzen für den Geltungsbereich festlegen und einen sogenannten Donut Scope schaffen.

Ich erkläre euch die Grundlagen dieses neuen Features und erprobe ihre Anwendung in einer Angular-Demo.

Demo: CSS Scope vs View Encapsulation

Meine Demo-Anwendung wurde mit Angular 17 erstellt. Sie enthält einen Header und einen Hauptabschnitt mit einigen Absätzen, Links und mehreren Rezeptblöcken.

Die Komponente app-recipe-card (gelber Hintergrund) verwendet die neue Scope-Funktion, um nur die HTML-Elemente in ihrem eigenen Teilbaum anzusprechen. Wichtig: Zum Zeitpunkt des Verfassens dieses Artikels funktioniert Scope nur in Chrome, Edge und Safari!

Zum Vergleich habe ich auch die Komponente app-recipe-card-old (blauer Hintergrund) definiert, welche die Standard-View-Encapsulation von Angular verwendet. Untersucht die Elemente mit den Entwicklerwerkzeugen eures Browsers.

Wie die Demo zeigt, können wir mit CSS-Scope einfache Selektoren mit geringer Spezifität und ohne zusätzliche Klassennamen schreiben. Schauen wir uns nun genauer an, wie das Ganze funktioniert.

Die Grundlagen von CSS-Scoping

Das neue CSS-Scope-Feature ist im Modul CSS Cascading and Inheritance Level 6 definiert, das zum Zeitpunkt des Verfassens dieses Beitrags noch ein Arbeitsentwurf ist. Es besagt:

A scope is a subtree or fragment of a document, which can be used by selectors for more targeted matching. A scope is formed by determining: The scoping root node, which acts as the upper bound of the scope, and optionally the scoping limit elements, which act as the lower bounds.

Das heißt, ihr müsst das Root-Element des DOM-Teilbaums definieren, auf den ihr eure Styles anwenden möchtet. Optional könnt ihr auch innere Elemente auflisten, welche die untere Grenze des Scopes darstellen.

Die @scope CSS-Regel

Die app-recipe-card Komponente in meiner Demo enthält Überschriften, Absätze und eine ungeordnete Liste. Wir stylen sie mit dem folgenden CSS-Code:

@scope (app-recipe-card) { h3 { color: var(--highlight-color); font-size: 1.3rem; } p { margin-block: 0 1em; } ul { list-style: square; margin: 0; padding-inline-start: 1.25rem; } li::marker{ color: var(--highlight-color); } }

Der obige @scope-Block definiert app-recipe-card als die Scoping Root, welche die obere Grenze des Teilbaums bestimmt, auf den wir abzielen. Nun beziehen sich alle enthaltenen Style-Regeln, wie h3 { ... }, nur auf diesen begrenzten Abschnitt des DOM.

Einen Donut Scope erzeugen

Manchmal reicht es nicht aus, nur einen Scoping-Root zu setzen. In Angular und React verschachteln wir meistens Komponenten innerhalb anderer Komponenten, um komplexe Benutzeroberflächen zu erstellen. Wie können wir sicherstellen, dass sich das Styling der Eltern-Komponente nicht auf ihre Kind-Komponenten auswirkt?

Die @scope-Regel akzeptiert auch ein Scoping Limit, das die untere Begrenzung bestimmt. In meiner Demo sind die Rezeptblöcke in der Komponente app-recipes-list eingebunden. Hier ist ein Teil des CSS-Codes:

@scope (app-recipes-list) to (app-recipe-card, app-recipe-card-old) { p { font-style: italic; } }

Auf diese Weise werden nur die von der übergeordneten Komponente app-recipes-list definierten Absätze kursiv gesetzt. Die Absätze innerhalb der untergeordneten Komponenten app-recipe-card und app-recipe-card-old sind davon nicht betroffen.

Diese Art von Scoping – mit einer oberen und unteren Grenze – wird als Donut Scope bezeichnet. Ich kann euch den Artikel “Limit the reach of your selectors with the CSS @scope at-rule” von Bramus Van Damme empfehlen. Er enthält tolle Visualisierungen verschiedener Scope-Szenarien.

Der :scope Selektor

Eine weitere nützliche Funktion ist der Selektor :scope. Damit könnt ihr innerhalb des @scope-Blocks auf das Root-Element des Scopings selbst abzielen. Hier ist ein Beispiel aus meiner Demo:

@scope (app-recipe-card) { :scope { --highlight-color: rgb(194 34 2); display: block; background: lightgoldenrodyellow; color: black; padding: 1rem; } }

Browser-Unterstützung

Wie bitte? Ihr findet das Scope-Feature total genial und wollt es sofort in allen Projekten einsetzen? Leider müsst ihr euch noch etwas gedulden.

Im Moment wird die @scope-Regel nur von Chrome 118+, Edge 118+ und Safari 17.4+ unterstützt. Firefox unterstützt das Feature noch nicht, aber Mozilla arbeitet aktiv an der Implementierung. Hoffen wir auf eine browserübergreifende Unterstützung im Laufe des Jahres!

Wie ihr @scope in einer Angular-Applikation verwendet

In Angular sind Komponenten-Styles standardmäßig gekapselt. Das Framework erstellt eigene HTML-Attribute wie _ngcontent-pmm-6, fügt sie in die generierten HTML-Elemente ein und ändert die CSS-Selektoren der Komponente so, dass sie nur auf die View der Komponente angewendet werden.

Wenn ihr stattdessen die @scope-Regel verwenden möchtet, müsst ihr View Encapsulation für jede Komponente manuell deaktivieren.

Die View Encapsulation abschalten

Um die View Encapsulation zu deaktivieren, müsst ihr im Dekorator der Komponente für encapsulation den Wert ViewEncapsulation.None setzen. Hier ein Beispiel aus meiner Demo:

@Component({ selector: 'app-recipe-card', standalone: true, templateUrl: './recipe-card.component.html', styleUrl: './recipe-card.component.css', encapsulation: ViewEncapsulation.None, }) export class RecipeCardComponent { @Input({ required: true }) recipe!: Recipe; }

Jetzt werden eure CSS-Selektoren nicht mehr mit eigenen Attributen erweitert und die Styles der Komponente werden global angewendet. Ihr solltet nun die @scope-Regel verwenden, um die Styles zu kapseln.

Vergleich des HTML- und CSS-Outputs

Wie wir gesehen haben, erfordert der Einsatz von CSS-Scope ein wenig Aufwand bei der Erstellung einer Komponente. Es ist komfortabler, einfach die Standard-View-Encapsulation von Angular zu verwenden.

Aber ich wage zu behaupten, dass die Vorteile von CSS-Scope diesen kleinen Aufwand rechtfertigen. Werfen wir einen Blick auf den HTML- und CSS-Code, der für die Komponente app-recipe-card in meiner Demo erzeugt wird:

/* HTML elements in the DOM */ <app-recipe-card> <h3>Pizza Margherita</h3> <p>The best pizza in town!</p> <h4>Ingredients</h4> <ul> <li>Cutting edge CSS features!</li> /* More list items */ </ul> </app-recipe-card> /* Generated CSS code (excerpt) */ @scope (app-recipe-card) { h3 { color: var(--highlight-color); font-size: 1.3rem; } }

Vergleichen wir das mit dem HTML- und CSS-Code, der für die Komponente app-recipe-card-old erzeugt wird:

<app-recipe-card-old _nghost-ng-c2291633987> <h3 _ngcontent-ng-c2291633987>Pizza Margherita (Old)</h3> <p _ngcontent-ng-c2291633987>The (second) best pizza in town!</p> <h4 _ngcontent-ng-c2291633987>Ingredients</h4> <ul _ngcontent-ng-c2291633987> <li _ngcontent-ng-c2291633987>DOM cluttering view encapsulation</li> /* More list items */ </ul> </app-recipe-card-old> /* Generated CSS code (excerpt) */ h3[_ngcontent-ng-c2291633987] { color: var(--highlight-color); font-size: 1.3rem; }

Stellt euch nun vor, ihr müsst eure Applikation debuggen. Die Rezeptblöcke werden nicht richtig gerendert. Ihr öffnet die Entwicklertools eures Browsers und untersucht das DOM und die angewandten Styles. Welcher Code wäre leichter zu lesen und zu verstehen?

Ich glaube, dass mich die unzähligen _ngcontent-ng-c2291633987-Attribute sehr ablenken würden. Es würde mir leichter fallen, den @scope (app-recipe-card) { h3 } Selektor zu verstehen als den h3[_ngcontent-ng-c2291633987] Selektor. 😉

Fazit

Zurück zu meiner ursprüngliche Frage: Wird das CSS Scope Feature die View Encapsulation von Angular ersetzen? Vielleicht. Ich weiß es wirklich nicht. Im Idealfall wird das Angular-Team seinen Standard-Mechanismus zur View Encapsulation anpassen und die @scope-Regel verwenden.

Unabhängig davon, was das Angular-Team tut: Es ist jetzt schon sehr einfach, die View Encapsulation für eine Komponente abzuschalten und die Styles innerhalb eines @scope-Blocks zu definieren. Der generierte HTML- und CSS-Code ist besser lesbar und viel einfacher zu debuggen. Außerdem verringert es die Dateigröße der Webanwendung.

Für mich steht fest: Sobald CSS-Scope browserübergreifend unterstützt wird, werde ich es in meinen Projekten einsetzen. 🤩

Erstellt am