Best Practices für CSS Scope in Angular-Applikationen

Erstellt am

Die neue @scope CSS-At-Regel wird seit Dezember 2025 von allen gängigen Browsern unterstützt. Dieses großartige neue Feature macht es super einfach, Styles auf bestimmte DOM-Teilbereiche, wie beispielsweise den Inhalt einer Komponente, anzuwenden.

In der Arbeit implementiere ich hauptsächlich Webanwendungen mit dem Angular-Framework. In den letzten Monaten habe ich begonnen, die View Encapsulation von Angular zu deaktivieren und stattdessen das native CSS-Feature zu verwenden. Nun möchte ich meine Erfahrungen und einige Best Practices mit euch teilen.

Eine Box mit mehreren Donuts in unterschiedlichen Farben.

Wenn ihr mit CSS Scope nicht vertraut seid, lest bitte zuerst meinen Artikel „Wird das CSS Scope Feature die View Encapsulation von Angular ersetzen?“. Darin erkläre ich die Grundlagen der @scope CSS-At-Regel und ihren Einsatz in Angular.

Demo-Projekt: CSS Scope Sandbox

Ich habe ein Projekt mit zwei Angular-Anwendungen erstellt, die denselben Inhalt anzeigen. Der einzige Unterschied besteht darin, dass die app-without-css-scope die standardmäßige View Encapsulation des Angular-Frameworks verwendet. Die app-with-css-scope hingegen deaktiviert diese Funktion und verwendet stattdessen die @scope Regel.

Hier ist die deployte Anwendung mit CSS Scope. Schaut euch den Code mit den Entwicklertools eures Browsers an:

Ich finde es toll, wie übersichtlich und aufgeräumt das DOM aussieht. Es ist viel einfacher, die Seitenstruktur zu verstehen und die Styles eines Elements zu überprüfen.

Die CSS-Selektoren, die ihr in den Browser-Entwicklertools seht, stimmen mit denen überein, die ihr im Quellcode definiert habt. Keine seltsamen Ergänzungen von benutzerdefinierten Attributen wie _ngcontent-xxx mehr.

Was sind also die Best Practices für CSS Scope, die ich empfehlen würde?

Best Practice 1: Effiziente Donut Scopes

Zunächst müssen wir die View Encapsulation für jede unserer Komponenten deaktivieren. Hier ist ein Beispiel aus meiner Demo:

@Component({
    selector: 'app-beer-item-list',
    encapsulation: ViewEncapsulation.None,
    ...
})
export class BeerItemList { ... }

Jetzt werden die CSS-Selektoren nicht mehr mit benutzerdefinierten Attributen erweitert und die Komponenten-Styles werden global angewendet. Als Nächstes müssen wir stattdessen die @scope At-Regel verwenden, um die Styles zu kapseln.

@scope (app-beer-item-list) {
    :scope {
        display: grid;
        ...
    }
}

Dadurch wird das Containerelement app-beer-item-list der Komponente als Scoping Root definiert, welche die obere Grenze des Teilbaums bestimmt, auf den wir abzielen.

Aber was ist mit den Inhalten, die von den Kind-Komponenten im Template gerendert werden? Die Komponente BeerItemList enthält die Kind-Komponente <app-beer-item-details> . Wir möchten nicht, dass die Styles der übergeordneten Komponente den Inhalt ihrer Kind-Komponenten beeinflusst.

Mit der @scope At-Regel können wir ein Scoping Limit definieren, das die untere Grenze festlegt. Diese Art von Scope – mit einer oberen und einer unteren Grenze – wird als Donut Scope bezeichnet:

@scope (app-beer-item-list) to (app-beer-item-details > *) {
    :scope {
        display: grid;
        ...
    }

    app-beer-item-details {
        background: var(--canvas-bg-color);
        ...
    }
}

Dieser Scope umfasst das <app-beer-item-details> Container-Element, schließt jedoch den Inhalt der Kind-Komponente aus.

Was passiert, wenn ihr eine Komponente stylt, die mehrere Kind-Komponenten im Template enthält? Die App-Komponente in meiner Demo enthält beispielsweise die Kind-Komponenten AppFooter , AppHeader und BeerItemList . Ihr müsstet jede Kind-Komponente als untere Grenze definieren, was sehr schnell lästig werden würde.

Um die Verwendung von CSS-Scope zu vereinfachen, habe ich die Angular-Direktive CustomNgHostDirective erstellt. Die Direktive fügt das benutzerdefinierte Attribut data-ng-host zum Container-HTML-Element einer Komponente hinzu. Wendet die Direktive einfach mit hostDirectives auf eine Komponente an:

@Component({
    selector: 'app-beer-item-details',
    encapsulation: ViewEncapsulation.None,
    hostDirectives: [CustomNgHostDirective],
    ...
})
export class BeerItemDetails { ... }

Jetzt können wir das Attribut data-ng-host für eine effiziente Definition der unteren Grenze einer @scope At-Regel verwenden. Zum Beispiel:

@scope (app-beer-item-list) to ([data-ng-host] > *) {
    :scope {
        display: grid;
        ...
    }

    app-beer-item-details {
        background: var(--canvas-bg-color);
        ...
    }
}

Best Practice 2: Ohne ::ng-deep in den Untiefen des DOMs wühlen

Wenn ihr die View Encapsulation von Angular verwendet, gelten Komponenten-Styles normalerweise nur für das HTML im eigenen Template der Komponente. Mit der Pseudoklasse ::ng-deep könnt ihr die View Encapsulation für eine bestimmte Regel deaktivieren. Diese Pseudoklasse ist jedoch seit einiger Zeit deprecated und sollte nicht mehr verwendet werden.

Mit CSS Scopes übernehmt ihr wieder die Kontrolle! Ihr könnt den Gültigkeitsbereich so definieren, dass ihr beispielsweise die internen Styles einer Drittanbieter-Bibliothek sicher überschreiben könnt.

Beispiel: Angenommen, ihr verwendet eine Angular Material-Tabelle, um tabellarische Daten anzuzeigen. Ihr seid mit dem Design weitgehend zufrieden, möchtet aber die Hintergrundfarben ein wenig anpassen. So könnten die Styles eurer Komponente aussehen:

@scope (app-fancy-table) {
    table[mat-table] {
        thead tr {
            background-color: lightblue;
        }
    }
}

Best Practice 3: Geteilte Styles als globale CSS-Klassen definieren

Ihr müsst nicht alle eure Styles mit CSS Scope in der CSS-Datei einer Komponente definieren. Besser ist, wenn ihr mehrmals genutzte Styles in globalen Dateien, z. B. im src/styles Ordner, ablegt und sie für die gesamte Anwendung bereitstellt.

Ich persönlich bevorzuge es, SCSS-Dateien mit allgemeinen Styles für jeden Inhaltstyp oder Element zu erstellen. Zum Beispiel: _details.scss , _forms.scss und _table.scss . Sie enthalten Styles für bestimmte HTML-Selektoren wie etwa details sowie globale CSS-Klassen wie z.B. .default-expansion-panel .

Um es den Komponenten zu erleichtern, diese allgemeinen Styles bei Bedarf zu überschreiben, habe ich sie in einem eigenen Cascade-Layer abgelegt:

@use "sass:meta";

@layer reset, general, components;

@layer general {
    @include meta.load-css("base/details");
    @include meta.load-css("base/forms");
    @include meta.load-css("base/table");
}

Erfahrt mehr über Cascade Layers in meinem Artikel „CSS Cascade Layers in Angular verwenden“.

Alternativ könnt ihr auch SCSS-Features wie Platzhalter oder Mixins verwenden, um wiederverwendbare Styles zu erstellen. Ich kann bestätigen, dass das Einfügen eines Mixins mit @include auch im Kontext der nativen @scope At-Regel funktioniert.

Leider hat die Verwendung eines SCSS-Platzhalters mit @extend für mich im Kontext von CSS Scope nicht funktioniert. Der SCSS-Präprozessor berücksichtigt offenbar noch nicht die @scope At-Regel, was zu ungültigen Styles führt. Zumindest war das bei mir mit Angular 21 der Fall. Vielleicht wird das in Zukunft noch behoben.

Fazit

Wie ihr seht, ist es ziemlich einfach, die View Encapsulation von Angular zu deaktivieren und Komponenten-Styles innerhalb einer @scope At-Regel zu definieren. Der generierte HTML- und CSS-Code ist viel besser lesbar und lässt sich viel einfacher debuggen.

Oder mit anderen Worten: Angulars View Encapsulation ist tot! Lang lebe CSS Scope! 🤩

Erstellt am