Every component brings its own CSS, and by default that CSS stays inside the component: a .title rule in one component doesn't touch a .title in another. Angular does this by rewriting your selectors when it compiles the component. The mechanism is called view encapsulation.
Knowing how the rewriting works answers the questions everyone asks in their first month: why the parent's CSS doesn't reach the child, why a style suddenly shows up on another page, and how to make a component themeable without ::ng-deep.
Where a component's styles live
Inline in styles, or in a file with styleUrl. Either way they belong to the component: Angular adds them to the page when the first instance appears and removes them when the last one is destroyed. Global CSS — resets, fonts, design tokens — goes in styles.scss, which is not encapsulated.
@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); }
`,
// …or a file: styleUrl: './note.css'
})
export class Note {}Emulated: the default
Angular gives each component a unique id and marks its elements with two attributes: _nghost-… on the host element and _ngcontent-… on every element of its template. Then it adds the attribute to every part of your selectors: .note becomes .note[_ngcontent-ng-c123]. Your rules can't get out of the component — but rules from outside still get in: global CSS and components without encapsulation apply as usual.
/* what you write */
:host { display: block; }
.note { border-left: 3px solid var(--accent); }
.list .note:hover { background: var(--surface-2); }
/* what the browser gets (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); }The four modes, live
ViewEncapsulation.None skips the rewriting: the styles become global as soon as the component appears. ShadowDom uses the browser's real Shadow DOM, so the page's CSS doesn't get in. ExperimentalIsolatedShadowDom (experimental since v21) goes one step further, as you'll see below.
import { ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-embed-widget',
// Emulated (default) · None · ShadowDom · ExperimentalIsolatedShadowDom
encapsulation: ViewEncapsulation.ShadowDom,
template: `…`,
styles: `…`,
})
export class EmbedWidget {}Elsewhere on the page
A paragraph with the class .enc-lab-note
Emulated (default)
None
Not mounted.
ShadowDom
ExperimentalIsolatedShadowDom
| Mode | Selectors in the browser | Where the style lives |
|---|
The None component's rule reaches the page and the Emulated component — and, surprisingly, the ShadowDom one too. In ShadowDom mode, Angular copies the styles of the app's other components into every shadow root. Emulated selectors don't match there, but global ones do. ExperimentalIsolatedShadowDom puts only the component's own styles in its shadow root. During SSR, both shadow modes are rendered as Emulated.
Which mode, when
| Mode | Your styles get out? | Page styles get in? | Use it for |
|---|---|---|---|
Emulated | no | yes | almost everything |
None | yes, they become global | yes | rarely: markup you don't control, a global style sheet shipped as a component |
ShadowDom | no | no — but Angular copies in the other components' styles | widgets embedded in other people's pages |
ExperimentalIsolatedShadowDom | no | no | full isolation (experimental) |
:host, :host() and :host-context()
The host element — <app-card> itself — belongs to the parent's template, so a normal selector inside the component can't reach it. :host can. The host starts as display: inline, so most components need :host { display: block }. :host(.active) applies when the host has a class; :host-context(.dark) applies when any ancestor has it.
/* the host element: <app-card> itself */
:host {
display: block; /* hosts are inline by default */
border: 1px solid var(--border);
}
/* a variant the component offers: <app-card class="compact"> */
:host(.compact) {
padding: 0.5rem;
}
/* when an ancestor has the class (emulated in Emulated mode) */
:host-context(.print) {
border-color: black;
}Prefer :host() for the variants a component offers. Angular emulates :host-context() in Emulated mode, but inside a real shadow root only Chromium supports it. For themes, CSS variables work everywhere.
Styling a child from the parent
The question everyone asks: “my CSS doesn't work on the child component!” The parent's selectors get the parent's attribute, while the elements inside the child carry the child's — so they never match. Here are the ways people try it, compiled live.
- Reached the button
- no
- Global rule
- no
Every part of the selector got the parent's attribute. The button inside the child carries the child's attribute, so nothing matches.
.attempt-plain app-buy-button button {
background: var(--bad);
}The browser gets—The last two survive refactors: the child publishes what can be changed, and the parent uses only that. ::ng-deep is deprecated but has no replacement, so Angular keeps it for cases where you don't own the child — styling a third-party library, for example. Even then, always start it with :host. Notice the specificity too: in Emulated mode, the child's .buy[_ngcontent-…] counts as two classes, so a rule from outside needs at least as much to win.
/* ✗ global: active on every page while the component exists */
::ng-deep .mat-mdc-button { text-transform: uppercase; }
/* ✓ if you really must: only inside this component */
:host ::ng-deep .mat-mdc-button { text-transform: uppercase; }Themes with CSS variables
Declare variables with fallbacks in the child — var(--buy-bg, var(--surface-2)) — and set them anywhere above it: in styles.scss for the whole app, on a container for one section, or with [style.--buy-bg] for a single instance. Dark mode is then just another set of values.
@Component({
selector: 'app-buy-button',
template: `<button class="buy"><ng-content /></button>`,
styles: `
/* the public styling API: variables with fallbacks… */
.buy {
background: var(--buy-bg, var(--surface-2));
color: var(--buy-fg, var(--text));
border-radius: var(--buy-radius, 8px);
}
/* …and variants the child supports */
:host(.danger) .buy {
--buy-bg: var(--bad-soft);
--buy-fg: var(--bad);
}
`,
})
export class BuyButton {}What belongs in styles.scss
Resets, fonts, design tokens (colors, spacing and radii as variables), and styles for HTML that isn't rendered by your components — innerHTML from a CMS, for example. Everything else belongs to the component that renders it.
Dynamically bound classes and styles are covered in Class & style bindings; how Shadow DOM behaves inside a Web Component, in Web Components with Angular Elements.
Common pitfalls
- A component that ignores width and margin. The host is
display: inlineby default. Add:host { display: block }. ViewEncapsulation.Noneto “fix” a selector. The styles become global and stay that way: they hit every element with that class for as long as the component exists.::ng-deepwithout:host. The same problem, one rule at a time.- Styling the child's internal markup. It works with
::ng-deepuntil the child changes its template. Ask the child for a variable or a variant instead. - Relying on
ShadowDomfor isolation. Angular copies the other components' styles into the shadow root. If you really need isolation, look atExperimentalIsolatedShadowDom.
Cheat sheet
styles / styleUrl the component's CSS; added with the first instance, removed with the last
Emulated (default) selectors get [_ngcontent-…]; nothing gets out, page CSS still gets in
None no rewriting: the styles are global while the component exists
ShadowDom real shadow root; Angular still copies the other components' styles in
Isolated (exp. v21) ExperimentalIsolatedShadowDom: only the component's own styles
:host the host element; inline by default → :host { display: block }
:host(.x) a variant of the host: <app-card class="x">
:host-context(.x) an ancestor has .x; avoid inside a real shadow root
::ng-deep stops scoping; deprecated; always :host ::ng-deep, only for code you don't own
CSS variables inherited across every boundary: the public styling API of a component
styles.scss resets, fonts, tokens, CMS/innerHTML markup — not component styles