Jede Komponente bringt ihr eigenes CSS mit, und standardmäßig bleibt dieses CSS in der Komponente: Eine .title-Regel in einer Komponente berührt kein .title in einer anderen. Angular erreicht das, indem es deine Selektoren beim Kompilieren der Komponente umschreibt. Der Mechanismus heißt View Encapsulation.
Wer weiß, wie das Umschreiben funktioniert, kann die Fragen beantworten, die jeder im ersten Monat stellt: warum das CSS des Elternteils nicht beim Kind ankommt, warum ein Style plötzlich auf einer anderen Seite auftaucht und wie man eine Komponente ohne ::ng-deep themefähig macht.
Wo die Styles einer Komponente liegen
Inline in styles oder in einer Datei mit styleUrl. So oder so gehören sie zur Komponente: Angular fügt sie der Seite hinzu, wenn die erste Instanz erscheint, und entfernt sie, wenn die letzte zerstört wird. Globales CSS — Resets, Schriften, Design-Tokens — gehört in styles.scss, das nicht gekapselt ist.
@Component({
selector: 'app-note',
template: `<p class="note"><ng-content /></p>`,
// Inline-Styles…
styles: `
:host { display: block; }
.note { padding: 0.5rem 0.75rem; border-left: 3px solid var(--accent); }
`,
// …oder eine Datei: styleUrl: './note.css'
})
export class Note {}Emulated: der Standard
Angular gibt jeder Komponente eine eindeutige ID und markiert ihre Elemente mit zwei Attributen: _nghost-… am Host-Element und _ngcontent-… an jedem Element ihres Templates. Dann hängt es das Attribut an jeden Teil deiner Selektoren: Aus .note wird .note[_ngcontent-ng-c123]. Deine Regeln kommen nicht aus der Komponente heraus — aber Regeln von außen kommen weiterhin hinein: Globales CSS und Komponenten ohne Kapselung wirken wie gewohnt.
/* was du schreibst */
:host { display: block; }
.note { border-left: 3px solid var(--accent); }
.list .note:hover { background: var(--surface-2); }
/* was der Browser bekommt (Emulated) */
[_nghost-ng-c123] { display: block; }
.note[_ngcontent-ng-c123] { border-left: 3px solid var(--accent); }
.list[_ngcontent-ng-c123] .note[_ngcontent-ng-c123]:hover { background: var(--surface-2); }Die vier Modi, live
ViewEncapsulation.None lässt das Umschreiben weg: Die Styles werden global, sobald die Komponente erscheint. ShadowDom nutzt das echte Shadow DOM des Browsers, also kommt das CSS der Seite nicht hinein. ExperimentalIsolatedShadowDom (experimentell seit v21) geht noch einen Schritt weiter, wie du unten siehst.
import { ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-embed-widget',
// Emulated (Standard) · None · ShadowDom · ExperimentalIsolatedShadowDom
encapsulation: ViewEncapsulation.ShadowDom,
template: `…`,
styles: `…`,
})
export class EmbedWidget {}Anderswo auf der Seite
Ein Absatz mit der Klasse .enc-lab-note
Emulated (Standard)
None
Nicht eingebunden.
ShadowDom
ExperimentalIsolatedShadowDom
| Modus | Selektoren im Browser | Wo der Style liegt |
|---|
Die Regel der None-Komponente erreicht die Seite und die Emulated-Komponente — und überraschenderweise auch die ShadowDom-Komponente. Im Modus ShadowDom kopiert Angular die Styles der anderen Komponenten der App in jeden Shadow Root. Emulated-Selektoren passen dort nicht, globale aber schon. ExperimentalIsolatedShadowDom legt nur die eigenen Styles der Komponente in ihren Shadow Root. Bei SSR werden beide Shadow-Modi als Emulated gerendert.
Welcher Modus, wann
| Modus | Kommen deine Styles raus? | Kommen Seiten-Styles rein? | Verwende ihn für |
|---|---|---|---|
Emulated | nein | ja | fast alles |
None | ja, sie werden global | ja | selten: Markup, das du nicht kontrollierst, ein globales Stylesheet als Komponente |
ShadowDom | nein | nein — aber Angular kopiert die Styles der anderen Komponenten hinein | Widgets, die in fremde Seiten eingebettet werden |
ExperimentalIsolatedShadowDom | nein | nein | vollständige Isolation (experimentell) |
:host, :host() und :host-context()
Das Host-Element — <app-card> selbst — gehört zum Template des Elternteils, daher erreicht es ein normaler Selektor in der Komponente nicht. :host schon. Der Host startet mit display: inline, also brauchen die meisten Komponenten :host { display: block }. :host(.active) greift, wenn der Host eine Klasse hat; :host-context(.dark), wenn irgendein Vorfahre sie hat.
/* das Host-Element: <app-card> selbst */
:host {
display: block; /* Hosts sind standardmäßig inline */
border: 1px solid var(--border);
}
/* eine Variante, die die Komponente anbietet: <app-card class="compact"> */
:host(.compact) {
padding: 0.5rem;
}
/* wenn ein Vorfahre die Klasse hat (im Modus Emulated emuliert) */
:host-context(.print) {
border-color: black;
}Nimm :host() für die Varianten, die eine Komponente anbietet. Angular emuliert :host-context() im Modus Emulated, aber in einem echten Shadow Root unterstützt es nur Chromium. Für Themes funktionieren CSS-Variablen überall.
Ein Kind vom Elternteil aus stylen
Die Frage, die jeder stellt: „Mein CSS wirkt nicht auf die Kindkomponente!“ Die Selektoren des Elternteils bekommen das Attribut des Elternteils, die Elemente im Kind tragen das des Kindes — sie passen also nie zusammen. Hier sind die Versuche, die man üblicherweise macht, live kompiliert.
- Hat den Button erreicht
- nein
- Globale Regel
- nein
Jeder Teil des Selektors hat das Attribut des Elternteils bekommen. Der Button im Kind trägt das Attribut des Kindes, also passt nichts.
.attempt-plain app-buy-button button {
background: var(--bad);
}Der Browser bekommt—Die letzten beiden überleben Refactorings: Das Kind veröffentlicht, was sich ändern lässt, und der Elternteil nutzt nur das. ::ng-deep ist deprecated, hat aber keinen Ersatz, deshalb behält Angular es für Fälle, in denen dir das Kind nicht gehört — etwa beim Stylen einer Fremdbibliothek. Auch dann: Fang immer mit :host an. Achte auch auf die Spezifität: Im Modus Emulated zählt .buy[_ngcontent-…] im Kind wie zwei Klassen, eine Regel von außen braucht also mindestens so viel, um zu gewinnen.
/* ✗ global: aktiv auf jeder Seite, solange die Komponente existiert */
::ng-deep .mat-mdc-button { text-transform: uppercase; }
/* ✓ wenn es wirklich sein muss: nur innerhalb dieser Komponente */
:host ::ng-deep .mat-mdc-button { text-transform: uppercase; }Themes mit CSS-Variablen
Deklariere im Kind Variablen mit Fallbacks — var(--buy-bg, var(--surface-2)) — und setze sie irgendwo darüber: in styles.scss für die ganze App, an einem Container für einen Bereich oder mit [style.--buy-bg] für eine einzelne Instanz. Der Dark Mode ist dann nur ein anderer Satz Werte.
@Component({
selector: 'app-buy-button',
template: `<button class="buy"><ng-content /></button>`,
styles: `
/* die öffentliche Styling-API: Variablen mit Fallbacks… */
.buy {
background: var(--buy-bg, var(--surface-2));
color: var(--buy-fg, var(--text));
border-radius: var(--buy-radius, 8px);
}
/* …und Varianten, die das Kind unterstützt */
:host(.danger) .buy {
--buy-bg: var(--bad-soft);
--buy-fg: var(--bad);
}
`,
})
export class BuyButton {}Was in styles.scss gehört
Resets, Schriften, Design-Tokens (Farben, Abstände und Radien als Variablen) und Styles für HTML, das nicht von deinen Komponenten gerendert wird — innerHTML aus einem CMS zum Beispiel. Alles andere gehört zu der Komponente, die es rendert.
Dynamisch gebundene Klassen und Styles behandelt Class- & Style-Bindings; wie sich Shadow DOM in einer Web Component verhält, Web Components mit Angular Elements.
Häufige Stolperfallen
- Eine Komponente, die width und margin ignoriert. Der Host ist standardmäßig
display: inline. Füg:host { display: block }hinzu. ViewEncapsulation.None, um einen Selektor zu „reparieren“. Die Styles werden global und bleiben es: Sie treffen jedes Element mit dieser Klasse, solange die Komponente existiert.::ng-deepohne:host. Dasselbe Problem, eine Regel nach der anderen.- Das interne Markup des Kindes stylen. Das funktioniert mit
::ng-deep, bis das Kind sein Template ändert. Bitte das Kind stattdessen um eine Variable oder eine Variante. - Sich bei der Isolation auf
ShadowDomverlassen. Angular kopiert die Styles der anderen Komponenten in den Shadow Root. Wenn du echte Isolation brauchst, schau dirExperimentalIsolatedShadowDoman.
Spickzettel
styles / styleUrl das CSS der Komponente; kommt mit der ersten Instanz, geht mit der letzten
Emulated (default) Selektoren bekommen [_ngcontent-…]; nichts geht raus, Seiten-CSS kommt weiter rein
None kein Umschreiben: Styles sind global, solange die Komponente existiert
ShadowDom echter Shadow Root; Angular kopiert trotzdem die Styles der anderen Komponenten hinein
Isolated (exp. v21) ExperimentalIsolatedShadowDom: nur die eigenen Styles der Komponente
:host das Host-Element; standardmäßig inline → :host { display: block }
:host(.x) eine Variante des Hosts: <app-card class="x">
:host-context(.x) ein Vorfahre hat .x; in einem echten Shadow Root vermeiden
::ng-deep stoppt die Begrenzung; deprecated; immer :host ::ng-deep, nur für fremden Code
CSS variables werden über jede Grenze vererbt: die öffentliche Styling-API einer Komponente
styles.scss Resets, Schriften, Tokens, CMS-/innerHTML-Markup — keine Komponenten-Styles