Responsive layout in Angular combines two tools: Angular CDK’s BreakpointObserver for reactive breakpoint detection in TypeScript, and CSS Grid/Flexbox for the actual layout. BreakpointObserver exposes the current screen size as an Observable — subscribing to it lets components reactively switch configurations (sidenav mode, column count, displayed table columns) without media query duplication between CSS and TypeScript.
BreakpointObserver and Responsive Sidenav
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs/operators';
@Component({
standalone: true,
imports: [MatSidenavModule, MatToolbarModule, MatButtonModule, MatIconModule,
RouterOutlet, RouterLink, AsyncPipe],
template: `
<mat-sidenav-container>
<mat-sidenav
[mode]="isHandset() ? 'over' : 'side'"
[opened]="!isHandset()"
#drawer>
<!-- nav links ────────────────────────────────────────────────── -->
</mat-sidenav>
<mat-sidenav-content>
<mat-toolbar>
@if (isHandset()) {
<button mat-icon-button (click)="drawer.toggle()">
<mat-icon>menu</mat-icon>
</button>
}
BlogApp
</mat-toolbar>
<router-outlet />
</mat-sidenav-content>
</mat-sidenav-container>
`,
})
export class AppShellComponent {
private breakpointObserver = inject(BreakpointObserver);
// Convert Observable to Signal for clean template binding
isHandset = toSignal(
this.breakpointObserver
.observe(Breakpoints.Handset) // phone-sized screens
.pipe(map(result => result.matches)),
{ initialValue: false }
);
isTablet = toSignal(
this.breakpointObserver
.observe([Breakpoints.Tablet, Breakpoints.TabletPortrait])
.pipe(map(result => result.matches)),
{ initialValue: false }
);
}
// ── Responsive post grid component ────────────────────────────────────────
@Component({
standalone: true,
template: `
<div class="post-grid" [class.grid-1col]="isHandset()"
[class.grid-2col]="isTablet()"
[class.grid-3col]="!isHandset() && !isTablet()">
@for (post of posts(); track post.id) {
<app-post-card [post]="post" />
}
</div>
`,
styles: [`
.post-grid { display: grid; gap: 1.5rem; }
.grid-1col { grid-template-columns: 1fr; }
.grid-2col { grid-template-columns: repeat(2, 1fr); }
.grid-3col { grid-template-columns: repeat(3, 1fr); }
/* CSS media queries as backup (no JS dependency) */
@media (max-width: 599px) { .post-grid { grid-template-columns: 1fr; } }
@media (600px - 959px) { .post-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 960px) { .post-grid { grid-template-columns: repeat(3, 1fr); } }
`],
})
export class PostGridComponent {
posts = input.required<PostSummaryDto[]>();
private bp = inject(BreakpointObserver);
isHandset = toSignal(this.bp.observe(Breakpoints.Handset).pipe(map(r => r.matches)), { initialValue: false });
isTablet = toSignal(this.bp.observe(Breakpoints.Tablet).pipe(map(r => r.matches)), { initialValue: false });
}
Breakpoints.Handset (phones), Breakpoints.Tablet (tablets), Breakpoints.Web (desktops), and orientation variants (Breakpoints.HandsetPortrait, Breakpoints.TabletLandscape). Use these named constants rather than raw pixel values ('(max-width: 599px)') for consistency with Material Design’s grid system. Custom breakpoints are also supported by passing media query strings directly.BreakpointObserver for purely CSS concerns (padding, font sizes, element visibility). If it can be expressed in a CSS media query, use CSS — it is faster (no JavaScript needed), simpler, and works during server-side rendering. Use BreakpointObserver only when the decision genuinely requires TypeScript: changing Angular component inputs, switching Angular component configurations, or conditionally rendering entire component trees (@if (isHandset()) { <mobile-nav /> } @else { <desktop-nav /> }).CDK Breakpoints Reference
| Breakpoint | Description | Approximate Width |
|---|---|---|
| Handset | Phones (portrait + landscape) | < 960px |
| HandsetPortrait | Phones portrait only | < 600px |
| Tablet | Tablets (portrait + landscape) | 600–1279px |
| Web | Desktop / large screens | > 960px |
| XSmall | Extra small screens | < 600px |
| Small | Small screens | 600–959px |
| Medium | Medium screens | 960–1279px |
| Large | Large screens | 1280–1919px |
Common Mistakes
Mistake 1 — Using BreakpointObserver for CSS-only concerns (unnecessary JavaScript)
❌ Wrong — using BreakpointObserver to hide/show elements that could use CSS @media queries; adds JavaScript overhead.
✅ Correct — use CSS media queries for layout/visibility; use BreakpointObserver for component configuration changes.
Mistake 2 — Not providing initialValue to toSignal for breakpoint (SSR undefined crash)
❌ Wrong — toSignal(bp.observe(...)) without initialValue; returns undefined on first render; template crashes.
✅ Correct — toSignal(..., { initialValue: false }) ensures a safe default before the Observable emits.