In almost any serious Angular app using Angular-Material it is necessary to control rendering of components based on CSS break-points. The package @angular/cdk/layout provides a BreakpointObserver service providing a number of break-point features to be monitored.

Being able to react on all possible options is major for larger commercial apps. But in most cases it is sufficient to render things according to basic view port width and device orientation.

View port width

In most cases it is sufficient to know, whether device view port width is

  • MOBILE: (max-width: 599.98px)
  • TABLET: (min-width: 600px) and (max-width: 959.98px)
  • DESKTOP: (min-width: 960px)

Device orientation

And - regarding device orientation, the BreakpointObserver of @angular/cdk/layout already delivers key values: DeviceOrientation = 'PORTRAIT' | 'LANDSCAPE'

Angular up to V16

For older Angular version we usually use a service providing Observables to monitor breakpoint and deviceOrientation.

import { BreakpointObserver, Breakpoints, BreakpointState } from '@angular/cdk/layout';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';


/**
 * Type for breakpoints
 * For the client we need to differentiate between the following breakpoints:
 * - MOBILE: XSmall
 * - TABLET: Small
 * - DESKTOP: Medium, Large, XLarge
 */
export type Breakpoint = 'MOBILE' | 'TABLET' | 'DESKTOP' | undefined;

/**
 * Type for device orientation
 */
export type DeviceOrientation = 'PORTRAIT' | 'LANDSCAPE' | undefined;


/**
 * Root level service providing breakpoint observation and customized
 * breakpoint values of the client.
 * The service monitors the angular breakpoint observer and provides
 * customized breakpoint values for the client as observable properties of the service
 * - breakpoint$: Observable<OosBreakpoint>
 *
 *
 * For the client we need to differentiate between the following breakpoints:
 * - MOBILE: XSmall
 * - TABLET: Small
 * - DESKTOP: Medium, Large, XLarge
 *
 * The Angular Material v14 breakpoints as of January 2023 are:
 * - (max-width: 599.98px): XSmall
 * - (min-width: 600px) and (max-width: 959.98px): Small
 * - (min-width: 960px) and (max-width: 1279.98px): Medium
 * - (min-width: 1280px) and (max-width: 1919.98px): Large
 * - (min-width: 1920px): XLarge
 */

@Injectable({
  providedIn: 'root'
})
export class BreakpointService {

 /**
   * Internally we use a behavior subject to provide the current breakpoint
   */
 private readonly _breakpoint$: BehaviorSubject<Breakpoint> = new BehaviorSubject<Breakpoint>(undefined);

 /**
  * Observable property providing the current breakpoint of client
  */
 public readonly breakpoint$: Observable<Breakpoint> = this._breakpoint$.asObservable();

 /**
  * Exported public current breakpoint value
  */
 public get breakpoint(): Breakpoint {  return this._breakpoint$.getValue(); }

 /**
  * Internally we use a behavior subject to provide the current device orientation
  */
 private readonly _deviceOrientation$: BehaviorSubject<DeviceOrientation> = new BehaviorSubject<DeviceOrientation>(undefined);

 /**
  * Observable property providing the current device orientation of client
  */
 public readonly deviceOrientation$: Observable<DeviceOrientation> = this._deviceOrientation$.asObservable();


 constructor(
   private readonly breakpointObserver: BreakpointObserver,
 ) {
   this.breakpointObserver.observe([
     Breakpoints.XSmall,
     Breakpoints.Small,
     Breakpoints.Medium,
     Breakpoints.Large,
     Breakpoints.XLarge,

     Breakpoints.Handset,
     Breakpoints.Tablet,
     Breakpoints.Web,

     Breakpoints.HandsetPortrait,
     Breakpoints.TabletPortrait,
     Breakpoints.WebPortrait,

     Breakpoints.HandsetLandscape,
     Breakpoints.TabletLandscape,
     Breakpoints.WebLandscape
   ]).subscribe((state: BreakpointState) => {
     if(state.breakpoints[Breakpoints.XSmall] === true) {
       this._breakpoint$.next('MOBILE');
     } else if(state.breakpoints[Breakpoints.Small] === true) {
       this._breakpoint$.next('TABLET');
     } else if(state.breakpoints[Breakpoints.Medium] === true || state.breakpoints[Breakpoints.Large] === true || state.breakpoints[Breakpoints.XLarge] === true) {
       this._breakpoint$.next('DESKTOP');
     } else {
       this._breakpoint$.next(undefined);
     }

     if(state.breakpoints[Breakpoints.HandsetPortrait] === true || state.breakpoints[Breakpoints.TabletPortrait] === true || state.breakpoints[Breakpoints.WebPortrait] === true) {
       this._deviceOrientation$.next('PORTRAIT');
     } else if(state.breakpoints[Breakpoints.HandsetLandscape] === true || state.breakpoints[Breakpoints.TabletLandscape] === true || state.breakpoints[Breakpoints.WebLandscape] === true) {
       this._deviceOrientation$.next('LANDSCAPE');
     } else {
       this._deviceOrientation$.next(undefined);
     }
   });
 }
}

Angular V17 and newer

In modern Angular versions the concept remains, but we provide signals instead of Observables:

import { BreakpointObserver, Breakpoints, BreakpointState } from '@angular/cdk/layout';
import { Injectable, Signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { BehaviorSubject } from 'rxjs';


/**
 * Type for breakpoints
 * For the client we need to differentiate between the following breakpoints:
 * - MOBILE: XSmall
 * - TABLET: Small
 * - DESKTOP: Medium, Large, XLarge
 */
export type Breakpoint = 'MOBILE' | 'TABLET' | 'DESKTOP' | undefined;

/**
 * Type for device orientation
 */
export type DeviceOrientation = 'PORTRAIT' | 'LANDSCAPE' | undefined;


/**
 * Root level service providing breakpoint observation and customized
 * breakpoint values of the client.
 * The service monitors the angular breakpoint observer and provides
 * customized breakpoint values for the client as signals.
 *
 * For the majority of clients we need to differentiate between the following breakpoints:
 * - MOBILE: XSmall
 * - TABLET: Small
 * - DESKTOP: Medium, Large, XLarge
 *
 * The Angular Material v14 breakpoints as of January 2023 are:
 * - (max-width: 599.98px): XSmall
 * - (min-width: 600px) and (max-width: 959.98px): Small
 * - (min-width: 960px) and (max-width: 1279.98px): Medium
 * - (min-width: 1280px) and (max-width: 1919.98px): Large
 * - (min-width: 1920px): XLarge
 */

@Injectable({
  providedIn: 'root'
})
export class BreakpointService {

  /**
    * Internally we use a behavior subject to provide the current breakpoint
    */
  private readonly _breakpoint$: BehaviorSubject<Breakpoint> = new BehaviorSubject<Breakpoint>(undefined);


  /**
   * Signal providing the current breakpoint value
   */
  public breakpoint: Signal<Breakpoint> = toSignal(this._breakpoint$, { initialValue: undefined });

  /**
   * Internally we use a behavior subject to provide the current device orientation
   */
  private readonly _deviceOrientation$: BehaviorSubject<DeviceOrientation> = new BehaviorSubject<DeviceOrientation>(undefined);

   /**
   * Signal providing the current device orientation value
   */
   public deviceOrientation: Signal<DeviceOrientation> = toSignal(this._deviceOrientation$, { initialValue: undefined });


  constructor(
    private readonly breakpointObserver: BreakpointObserver,
  ) {
    this.breakpointObserver.observe([
      Breakpoints.XSmall,
      Breakpoints.Small,
      Breakpoints.Medium,
      Breakpoints.Large,
      Breakpoints.XLarge,

      Breakpoints.Handset,
      Breakpoints.Tablet,
      Breakpoints.Web,

      Breakpoints.HandsetPortrait,
      Breakpoints.TabletPortrait,
      Breakpoints.WebPortrait,

      Breakpoints.HandsetLandscape,
      Breakpoints.TabletLandscape,
      Breakpoints.WebLandscape
    ]).subscribe((state: BreakpointState) => {
      if (state.breakpoints[Breakpoints.XSmall] === true) {
        this._breakpoint$.next('MOBILE');
      } else if (state.breakpoints[Breakpoints.Small] === true) {
        this._breakpoint$.next('TABLET');
      } else if (state.breakpoints[Breakpoints.Medium] === true || state.breakpoints[Breakpoints.Large] === true || state.breakpoints[Breakpoints.XLarge] === true) {
        this._breakpoint$.next('DESKTOP');
      } else {
        this._breakpoint$.next(undefined);
      }

      if (state.breakpoints[Breakpoints.HandsetPortrait] === true || state.breakpoints[Breakpoints.TabletPortrait] === true || state.breakpoints[Breakpoints.WebPortrait] === true) {
        this._deviceOrientation$.next('PORTRAIT');
      } else if (state.breakpoints[Breakpoints.HandsetLandscape] === true || state.breakpoints[Breakpoints.TabletLandscape] === true || state.breakpoints[Breakpoints.WebLandscape] === true) {
        this._deviceOrientation$.next('LANDSCAPE');
      } else {
        this._deviceOrientation$.next(undefined);
      }
    });
  }
}