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'
Current Angular versions
With current Angular version (v20) we aim to use Signals to provide break-point information instead of Observables. Please find an implementation of the BreakpointService
using Angular Signals below:
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { computed, inject, Injectable, Signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
/**
* The BreakpointService is an Angular service that provides reactive information about the current browser viewport size and device orientation, using Angular CDK's BreakpointObserver and signals.
* Key features:
* - Breakpoint Types:
* - Breakpoint: Abstracts viewport into 'MOBILE', 'TABLET', or 'DESKTOP'.
* - BreakpointNative: Uses Angular Material's native breakpoints ('XSmall', 'Small', 'Medium', 'Large', 'XLarge').
* - DeviceOrientation: Indicates 'PORTRAIT' or 'LANDSCAPE'.
* - Reactive Signals:
* - The service uses signals (Signal<T>) to expose the current breakpoint, native breakpoint, and device orientation. These update automatically when the viewport changes.
* - Breakpoint Detection:
* - Internally, it observes a set of breakpoints (including portrait/landscape variants) using BreakpointObserver.
* - The computed signals map the observed breakpoints to the custom types.
* - Usage:
* - Components can inject this service and subscribe to its signals to reactively adapt their UI to viewport changes (e.g., show mobile layout, tablet layout, etc.).
*/
@Injectable({
providedIn: 'root'
})
export class BreakpointService {
/**
* DI
*/
#breakpointObserver = inject(BreakpointObserver);
/**
* Private breakpoint state signal
* - used to create the public breakpoint signal properties
*/
#breakpointState = toSignal(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
]));
/**
* Reactive signal for the current abstracted breakpoint.
*
* Possible values:
* - 'MOBILE': XSmall screens (phones)
* - 'TABLET': Small screens (tablets)
* - 'DESKTOP': Medium, Large, or XLarge screens (desktops)
*
* Updates automatically when the viewport size changes.
*
* @readonly
*/
readonly breakpoint: Signal<'MOBILE' | 'TABLET' | 'DESKTOP'> = computed(() => {
const state = this.#breakpointState();
if (!state) return 'DESKTOP'; // SSR fallback: assume desktop
if (state?.breakpoints[Breakpoints.Small] === true) { return 'TABLET'; }
else if (state?.breakpoints[Breakpoints.Medium] === true || state?.breakpoints[Breakpoints.Large] === true || state?.breakpoints[Breakpoints.XLarge] === true) { return 'DESKTOP'; }
return 'MOBILE';
});
/**
* Reactive signal for the current native breakpoint value.
*
* Possible values:
* - 'XSmall': max-width 599.98px
* - 'Small': 600px - 959.98px
* - 'Medium': 960px - 1279.98px
* - 'Large': 1280px - 1919.98px
* - 'XLarge': min-width 1920px
*
* Updates automatically when the viewport size changes.
*
* @readonly
*/
readonly breakpointNative: Signal<'XSmall' | 'Small' | 'Medium' | 'Large' | 'XLarge'> = computed(() => {
const state = this.#breakpointState();
if (!state) return 'XLarge'; // SSR fallback: assume largest
if (state?.breakpoints[Breakpoints.Small] === true) { return 'Small'; }
else if (state?.breakpoints[Breakpoints.Medium] === true) { return 'Medium'; }
else if (state?.breakpoints[Breakpoints.Large] === true) { return 'Large'; }
else if (state?.breakpoints[Breakpoints.XLarge] === true) { return 'XLarge'; }
return 'XSmall';
});
/**
* Reactive signal for the current device orientation.
*
* Possible values:
* - 'PORTRAIT': Device is in portrait mode
* - 'LANDSCAPE': Device is in landscape mode
*
* Updates automatically when the viewport orientation changes.
*
* @readonly
*/
public deviceOrientation: Signal<'PORTRAIT' | 'LANDSCAPE'> = computed(() => {
const state = this.#breakpointState();
if (!state) return 'LANDSCAPE'; // SSR fallback: assume landscape
if (state?.breakpoints[Breakpoints.HandsetLandscape] === true || state?.breakpoints[Breakpoints.TabletLandscape] === true || state?.breakpoints[Breakpoints.WebLandscape] === true) {
return 'LANDSCAPE';
}
return 'PORTRAIT';
});
}
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);
}
});
}
}