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.
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