Responsive Layout — Angular CDK Layout and Breakpoint Observer

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 });
}
Note: Angular CDK provides named breakpoints that mirror Material Design’s responsive grid spec: 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.
Tip: Always include CSS media query fallbacks alongside TypeScript breakpoint detection. CSS media queries work without JavaScript, making the layout correct on first paint before Angular bootstraps. TypeScript breakpoints control component behaviour (sidenav mode, column count signals) after Angular starts. The two approaches complement each other: CSS for pure layout, TypeScript for component configuration. This dual approach also improves server-side rendering compatibility.
Warning: Avoid using 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.

🧠 Test Yourself

A desktop user resizes the browser window to phone width mid-session. The isHandset signal is derived from BreakpointObserver. Does the sidenav mode update reactively?