1. Component Styles & :host / :host-context
A component's stylesheet, by default, only applies to that component's own template —
not to children, not to the rest of the app. :host is how a component
styles its own root element from the inside, since that element doesn't appear in the
component's own template.
:host {
display: block; /* components render as an unstyled inline element by default */
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
}
:host(.is-selected) {
border-color: var(--accent);
}
:host-context(.theme-dark) {
background: #1a1a1a;
color: #eee;
}
:host(.is-selected) applies only when the host element itself has that
class — set via [class.is-selected] from a parent. :host-context()
is different: it matches based on an ancestor, anywhere up the DOM tree, which
is exactly how theme classes applied way up on <body> can still affect
a deeply nested component's styles.
2. View Encapsulation Modes
Angular scopes component styles by rewriting selectors with a unique attribute behind
the scenes — that's the default, and it's configurable per component via the
encapsulation option.
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-card',
templateUrl: './card.html',
styleUrl: './card.css',
encapsulation: ViewEncapsulation.Emulated, // default -- scoped via attribute selectors
})
export class Card {}
Emulated(default) — Angular adds a unique attribute to your component's elements and rewrites your CSS selectors to match it. Behaves like real scoping without needing actual Shadow DOM support.ShadowDom— uses the browser's real Shadow DOM. True isolation (global styles can't leak in either), but styles like your app's design tokens won't reach in unless passed via CSS custom properties, which do cross the shadow boundary.None— no scoping at all; the component's styles become global. Occasionally useful for a component whose entire job is to define global styles, but easy to misuse by accident.
3. Host Bindings & Host Listeners
The host object in @Component lets you bind properties and
listen for events directly on a component's own host element — no wrapper
<div> required inside the template.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-dropdown',
templateUrl: './dropdown.html',
host: {
'[class.is-open]': 'isOpen()',
'[attr.aria-expanded]': 'isOpen()',
'(document:click)': 'onDocumentClick($event)',
},
})
export class Dropdown {
isOpen = signal(false);
onDocumentClick(event: MouseEvent) {
if (!(event.target as HTMLElement).closest('app-dropdown')) {
this.isOpen.set(false);
}
}
}
This is the same underlying mechanism as the older @HostBinding/@HostListener
decorators — the host object is simply the current, preferred way to write
it, keeping all host-level bindings in one place instead of scattered across decorated
class members.
4. ng-content, Multi-Slot Projection & Fallback Content
Content projection lets a parent pass arbitrary markup into a component, which renders
it wherever <ng-content> appears in its template — the same idea as
JavaScript's children prop in other component frameworks.
<div class="panel">
<header class="panel__header">
<ng-content select="[panel-header]">
<span>Untitled panel</span> <!-- fallback if nothing is projected -->
</ng-content>
</header>
<div class="panel__body">
<ng-content></ng-content> <!-- default slot: anything with no matching selector -->
</div>
</div>
<app-panel>
<h3 panel-header>Account Settings</h3>
<p>This content lands in the default slot.</p>
</app-panel>
select="[panel-header]" matches any projected element carrying that
attribute; content with no matching select anywhere falls through to the
plain, selector-less <ng-content> — which is why order and specificity
of your select attributes matters once a component has more than one slot.
5. Responsive Layout with the Angular CDK Layout Module
CSS media queries handle most responsive styling, but sometimes a component needs to
know the current breakpoint in TypeScript — to render entirely different markup
on mobile, for instance. BreakpointObserver is the CDK's answer.
import { Component, inject } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
@Component({
selector: 'app-responsive-nav',
templateUrl: './responsive-nav.html',
})
export class ResponsiveNav {
private breakpointObserver = inject(BreakpointObserver);
isMobile = toSignal(
this.breakpointObserver.observe(Breakpoints.Handset).pipe(
map((result) => result.matches)
),
{ initialValue: false }
);
}
You met toSignal() only in passing back in Week 5 — this is a preview of
Week 16, where bridging RxJS and signals gets a full lesson. For now, notice the shape:
an Observable-based CDK API, converted to a signal at the boundary so the template only
ever deals with a plain, callable value.
6. Building a Small Design-System Component
Everything this week combines into one recognizable pattern: a component with scoped styles, host bindings for state, and projected content for flexibility. A button component is the simplest realistic example:
import { Component, input } from '@angular/core';
@Component({
selector: 'app-button',
template: `<ng-content />`,
host: {
'class': 'app-button',
'[class.app-button--primary]': "variant() === 'primary'",
'[attr.disabled]': 'disabled() || null',
},
})
export class Button {
variant = input<'primary' | 'secondary'>('secondary');
disabled = input(false);
}
Notice there's no wrapping element in the template at all — host bindings
style the component's own tag directly, and <ng-content /> (Angular's
self-closing shorthand) projects whatever the caller puts inside
<app-button>...</app-button>. This is the exact shape you'll
extend in this week's exercise.
7. Hands-on Exercise
Build a modal with named projection slots and CDK focus trapping
Combine content projection, host bindings, and one new CDK import to build a reusable modal.
Requirements:
- Create a
Modalcomponent with three named slots —[modal-header],[modal-body], and[modal-footer]— each with sensible fallback content if the caller omits it. - An
openinput (model(), so it's two-way bindable) controls visibility via a host class, e.g.[class.is-open]. - Install
@angular/cdkif you haven't yet, importA11yModule, and apply thecdkTrapFocusdirective to the modal's content wrapper so Tab key focus can't escape the modal while it's open. - Add a host
(document:keydown.escape)listener that closes the modal. - Use the component from Section 6's
Buttonin the footer slot for a "Close" action, proving your two components compose together.
If projected content isn't landing in the right slot, double-check the attribute selector matches exactly — select="[modal-header]" only matches elements carrying that exact attribute, not a class or a tag name.
8. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What's the difference between :host(.is-selected) and :host-context(.theme-dark)?
What's the difference between :host(.is-selected) and :host-context(.theme-dark)?
:host(.is-selected) matches when the component's own host element carries that class. :host-context(.theme-dark) matches when that class exists on any ancestor, anywhere up the tree — it's how a global theme class on <body> can still style a component nested many levels deep.
Q2
Why might ViewEncapsulation.ShadowDom cause a component's colors to look wrong even though the CSS looks correct?
Why might ViewEncapsulation.ShadowDom cause a component's colors to look wrong even though the CSS looks correct?
Real Shadow DOM is genuinely isolated — global stylesheets and app-wide design tokens defined as regular CSS classes can't cross into it. Only CSS custom properties (--variable-name) cross the shadow boundary, so a Shadow DOM component needs its colors driven by custom properties, not classes set globally.
Q3
In the Panel example, what happens to a projected element that matches no select attribute on any ng-content?
In the Panel example, what happens to a projected element that matches no select attribute on any ng-content?
It falls through to the plain, selector-less <ng-content></ng-content> — the "default slot." If a component has no such default slot at all, unmatched projected content is simply not rendered anywhere.
Q4
Why convert BreakpointObserver's Observable to a signal with toSignal() instead of just subscribing manually in the component?
Why convert BreakpointObserver's Observable to a signal with toSignal() instead of just subscribing manually in the component?
A manual subscription needs its own field to store the latest value, plus an ngOnDestroy to unsubscribe and avoid a memory leak. toSignal() handles both automatically — it manages the subscription's lifecycle and gives the template a plain, callable signal, consistent with everything else you're reading in the component.