Chaque composant apporte son propre CSS et, par défaut, ce CSS reste dans le composant : une règle .title d’un composant ne touche pas le .title d’un autre. Angular y parvient en réécrivant vos sélecteurs quand il compile le composant. Ce mécanisme s’appelle la view encapsulation.
Comprendre cette réécriture répond aux questions que tout le monde pose le premier mois : pourquoi le CSS du parent n’atteint pas l’enfant, pourquoi un style apparaît soudain sur une autre page, et comment rendre un composant personnalisable sans ::ng-deep.
Où vivent les styles d’un composant
En ligne dans styles, ou dans un fichier avec styleUrl. Dans les deux cas, ils appartiennent au composant : Angular les ajoute à la page quand la première instance apparaît et les retire quand la dernière est détruite. Le CSS global — resets, polices, design tokens — va dans styles.scss, qui n’est pas encapsulé.
@Component({
selector: 'app-note',
template: `<p class="note"><ng-content /></p>`,
// styles en ligne…
styles: `
:host { display: block; }
.note { padding: 0.5rem 0.75rem; border-left: 3px solid var(--accent); }
`,
// …ou un fichier : styleUrl: './note.css'
})
export class Note {}Emulated : le mode par défaut
Angular donne à chaque composant un identifiant unique et marque ses éléments avec deux attributs : _nghost-… sur l’élément hôte et _ngcontent-… sur chaque élément de son template. Il ajoute ensuite l’attribut à chaque partie de vos sélecteurs : .note devient .note[_ngcontent-ng-c123]. Vos règles ne peuvent pas sortir du composant — mais les règles extérieures entrent toujours : le CSS global et les composants sans encapsulation s’appliquent normalement.
/* ce que vous écrivez */
:host { display: block; }
.note { border-left: 3px solid var(--accent); }
.list .note:hover { background: var(--surface-2); }
/* ce que reçoit le navigateur (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); }Les quatre modes, en direct
ViewEncapsulation.None saute la réécriture : les styles deviennent globaux dès que le composant apparaît. ShadowDom utilise le vrai Shadow DOM du navigateur, donc le CSS de la page n’entre pas. ExperimentalIsolatedShadowDom (expérimental depuis la v21) va encore plus loin, comme vous le verrez ci-dessous.
import { ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-embed-widget',
// Emulated (par défaut) · None · ShadowDom · ExperimentalIsolatedShadowDom
encapsulation: ViewEncapsulation.ShadowDom,
template: `…`,
styles: `…`,
})
export class EmbedWidget {}Ailleurs sur la page
Un paragraphe avec la classe .enc-lab-note
Emulated (par défaut)
None
Pas monté.
ShadowDom
ExperimentalIsolatedShadowDom
| Mode | Sélecteurs dans le navigateur | Où vit le style |
|---|
La règle du composant None atteint la page et le composant Emulated — et, plus surprenant, le composant ShadowDom aussi. En mode ShadowDom, Angular copie dans chaque shadow root les styles des autres composants de l’application. Les sélecteurs Emulated n’y correspondent à rien, mais les sélecteurs globaux, si. ExperimentalIsolatedShadowDom ne met dans son shadow root que les styles du composant lui-même. En SSR, les deux modes shadow sont rendus comme Emulated.
Quel mode, quand
| Mode | Vos styles sortent ? | Les styles de la page entrent ? | À utiliser pour |
|---|---|---|---|
Emulated | non | oui | presque tout |
None | oui, ils deviennent globaux | oui | rarement : du balisage que vous ne contrôlez pas, une feuille de style globale livrée comme un composant |
ShadowDom | non | non — mais Angular y copie les styles des autres composants | des widgets intégrés dans les pages d’autres sites |
ExperimentalIsolatedShadowDom | non | non | une isolation complète (expérimental) |
:host, :host() et :host-context()
L’élément hôte — <app-card> lui-même — appartient au template du parent : un sélecteur ordinaire dans le composant ne peut pas l’atteindre. :host, si. L’hôte commence en display: inline, donc la plupart des composants ont besoin de :host { display: block }. :host(.active) s’applique quand l’hôte a une classe ; :host-context(.dark), quand un ancêtre l’a.
/* l’élément hôte : <app-card> lui-même */
:host {
display: block; /* les hôtes sont inline par défaut */
border: 1px solid var(--border);
}
/* une variante proposée par le composant : <app-card class="compact"> */
:host(.compact) {
padding: 0.5rem;
}
/* quand un ancêtre a la classe (émulé en mode Emulated) */
:host-context(.print) {
border-color: black;
}Préférez :host() pour les variantes qu’un composant propose. Angular émule :host-context() en mode Emulated, mais dans un vrai shadow root seul Chromium le prend en charge. Pour les thèmes, les variables CSS fonctionnent partout.
Styler un enfant depuis le parent
La question que tout le monde pose : « mon CSS ne marche pas sur le composant enfant ! » Les sélecteurs du parent reçoivent l’attribut du parent, tandis que les éléments de l’enfant portent celui de l’enfant — ils ne correspondent donc jamais. Voici les tentatives habituelles, compilées en direct.
- A atteint le bouton
- non
- Règle globale
- non
Chaque partie du sélecteur a reçu l’attribut du parent. Le bouton de l’enfant porte l’attribut de l’enfant, donc rien ne correspond.
.attempt-plain app-buy-button button {
background: var(--bad);
}Le navigateur reçoit—Les deux dernières survivent aux refactorisations : l’enfant publie ce qui peut changer, et le parent n’utilise que cela. ::ng-deep est déprécié mais n’a pas de remplaçant, donc Angular le garde pour les cas où l’enfant ne vous appartient pas — styler une bibliothèque tierce, par exemple. Même alors, commencez-le toujours par :host. Attention aussi à la spécificité : en mode Emulated, le .buy[_ngcontent-…] de l’enfant compte comme deux classes, donc une règle extérieure a besoin d’au moins autant pour gagner.
/* ✗ global : actif sur toutes les pages tant que le composant existe */
::ng-deep .mat-mdc-button { text-transform: uppercase; }
/* ✓ si vraiment il le faut : seulement à l’intérieur de ce composant */
:host ::ng-deep .mat-mdc-button { text-transform: uppercase; }Des thèmes avec des variables CSS
Déclarez dans l’enfant des variables avec des valeurs de repli — var(--buy-bg, var(--surface-2)) — et définissez-les n’importe où au-dessus : dans styles.scss pour toute l’application, sur un conteneur pour une section, ou avec [style.--buy-bg] pour une seule instance. Le mode sombre n’est alors qu’un autre jeu de valeurs.
@Component({
selector: 'app-buy-button',
template: `<button class="buy"><ng-content /></button>`,
styles: `
/* l’API de style publique : des variables avec repli… */
.buy {
background: var(--buy-bg, var(--surface-2));
color: var(--buy-fg, var(--text));
border-radius: var(--buy-radius, 8px);
}
/* …et les variantes que l’enfant prend en charge */
:host(.danger) .buy {
--buy-bg: var(--bad-soft);
--buy-fg: var(--bad);
}
`,
})
export class BuyButton {}Ce qui va dans styles.scss
Les resets, les polices, les design tokens (couleurs, espacements et rayons en variables) et les styles du HTML qui n’est pas rendu par vos composants — le innerHTML d’un CMS, par exemple. Tout le reste appartient au composant qui le rend.
Les classes et styles liés dynamiquement sont dans Bindings de classe et de style ; le comportement du Shadow DOM dans un Web Component, dans Web Components avec Angular Elements.
Pièges fréquents
- Un composant qui ignore width et margin. L’hôte est en
display: inlinepar défaut. Ajoutez:host { display: block }. ViewEncapsulation.Nonepour « réparer » un sélecteur. Les styles deviennent globaux et le restent : ils touchent tous les éléments qui ont cette classe tant que le composant existe.::ng-deepsans:host. Le même problème, une règle à la fois.- Styler le balisage interne de l’enfant. Ça marche avec
::ng-deepjusqu’au jour où l’enfant change son template. Demandez plutôt à l’enfant une variable ou une variante. - Compter sur
ShadowDompour l’isolation. Angular copie dans le shadow root les styles des autres composants. Si vous avez vraiment besoin d’isolation, regardezExperimentalIsolatedShadowDom.
Aide-mémoire
styles / styleUrl le CSS du composant ; ajouté avec la première instance, retiré avec la dernière
Emulated (default) les sélecteurs reçoivent [_ngcontent-…] ; rien ne sort, le CSS de la page entre toujours
None pas de réécriture : les styles sont globaux tant que le composant existe
ShadowDom vrai shadow root ; Angular y copie quand même les styles des autres composants
Isolated (exp. v21) ExperimentalIsolatedShadowDom : seulement les styles du composant
:host l’élément hôte ; inline par défaut → :host { display: block }
:host(.x) une variante de l’hôte : <app-card class="x">
:host-context(.x) un ancêtre a .x ; à éviter dans un vrai shadow root
::ng-deep arrête la délimitation ; déprécié ; toujours :host ::ng-deep, seulement pour du code tiers
CSS variables héritées à travers toutes les frontières : l’API de style publique d’un composant
styles.scss resets, polices, tokens, balisage de CMS/innerHTML — pas les styles des composants