Media Queries with RxJS
Media Queries | Practical examples with RxJS
- Authors
- Gary Großgarten
- Published at
tl;dr
It's super easy to handle Media Queries programmatically with RxJS! 🤗
Introduction
Media Queries in CSS
Media Queries are essential tools when building responsive layouts on the web. They are commonly used to hide / show / alter parts of the UI depending on the viewport dimensions or to switch between themes based on user preferences (e.g. Darkmode 🌙).
In CSS Media Queries are used like so.
@media (max-width: 767px) {
/* apply styles */
}
Although this is already pretty great, we sometimes want to handle the state of a media query programmatically. For example, preventing the render of some components or dom elements on certain viewport sizes instead of just hiding things with display: none
could lead to a better performance and less network requests to your server.
Media Queries in JavaScript
The vanilla javascript way of implementing such a functionality would be to use the window's matchMedia
function. The function takes a string query and returns a MediaQueryList
that can be used to get the current result of the query and listen to changes of the media query.
const mediaQueryList = window.matchMedia(`(min-width: 767px)`);
console.log(mediaQueryList.matches); // true or false
mediaQueryList.addEventListener('change', (event) =>
console.log(event.matches) // true or false
);
// don't forget to remove the event listener ;)
Media Queries with RxJS
As an Angular developer, I make heavy use of RxJS in my applications. To neatly integrate Media Queries in my workflow I came up with the media Observable.
import { fromEvent, Observable } from 'rxjs';
import { startWith, map } from 'rxjs/operators';
export function media(query: string): Observable<boolean> {
const mediaQuery = window.matchMedia(query);
return fromEvent<MediaQueryList>(mediaQuery, 'change').pipe(
startWith(mediaQuery),
map((list: MediaQueryList) => list.matches)
);
}
// Usage
media('(max-width: 767px)').subscribe((matches) =>
console.log(matches) // true or false
);
We use RxJS fromEvent
Observable creation function to listen for all changes to the MediaQueryList
. To get the inital MediaQueryList
, we use the startWith
operator. The MediaQueryList
is then mapped to the actual result of the query by using the matches
function.
The media
function returns a Observable<boolean>
stream which can be used to subscribe to the current state and
future changes of the media query.
Demos
The demos were built in an Angular workspace. As you can see, I make use of Angular's async
pipe to subscribe to the media Observables.
Breakpoints demo
In this demo we use the media Observable to track the default Tailwind CSS screen breakpoints. Depending on the viewport dimensions certain elements are hidden using *ngIf
. Resize your browser window to see the breakpoints change.
import { Component, HostBinding } from '@angular/core';
import { media } from './media';
@Component({
selector: 'demo-breakpoints',
templateUrl: 'breakpoints.component.ts',
})
export class BreakPointsComponent {
sm$ = media(`(max-width: 767px)`);
md$ = media(`(min-width: 768px) and (max-width: 1023px)`);
lg$ = media(`(min-width: 1024px) and (max-width: 1279px)`);
xl$ = media(`(min-width: 1280px) and (max-width: 1535px)`);
xl2$ = media(`(min-width: 1536px)`);
}
<div *ngIf="sm$ | async">sm</div>
<div *ngIf="md$ | async">md</div>
<div *ngIf="lg$ | async">lg</div>
<div *ngIf="xl$ | async">xl</div>
<div *ngIf="xl2$ | async">2xl</div>
Device / Browser preferences demo
This demo watches device / browser preferences and the viewport orientation.
import { Component, HostBinding } from '@angular/core';
import { media } from './media';
@Component({
selector: 'demo-preferences',
templateUrl:'preferences.component.html',
})
export class PreferencesComponent {
@HostBinding('class') class = 'block relative space-y-4';
prefersLight$ = media('(prefers-color-scheme: light)');
prefersDark$ = media('(prefers-color-scheme: dark)');
prefersReducedMotion$ = media('(prefers-reduced-motion:reduce)');
prefersReducedTransparency$ = media('(prefers-reduced-transparency:reduce)');
prefersReducedData$ = media('(prefers-reduced-data: reduce)');
prefersContrast$ = media('(prefers-contrast:high)');
portrait$ = media('(orientation: portrait)');
landscape$ = media('(orientation: landscape)');
}
<div class="p-4 rounded-xl bg-canvas-shade grid md:grid-cols-2 gap-4">
<div [ngClass]="{ 'opacity-30': !(prefersDark$ | async) }">
prefers-color-scheme: dark
</div>
<div [ngClass]="{ 'opacity-30': !(prefersLight$ | async) }">
prefers-color-scheme: light
</div>
<div [ngClass]="{ 'opacity-30': !(prefersReducedMotion$ | async) }">
prefers-reduced-motion: reduce
</div>
<div [ngClass]="{ 'opacity-30': !(prefersReducedTransparency$ | async) }">
prefers-reduced-transparency: reduce
</div>
<div [ngClass]="{ 'opacity-30': !(prefersReducedData$ | async) }">
prefers-reduced-data: reduce
</div>
<div [ngClass]="{ 'opacity-30': !(prefersContrast$ | async) }">
prefers-contract: high
</div>
<div [ngClass]="{ 'opacity-30': !(portrait$ | async) }">
orientation: portrait
</div>
<div [ngClass]="{ 'opacity-30': !(landscape$ | async) }">
orientation: landscape
</div>
</div>
If you have further questions, feel free to contact me!
Sponsor us
Did you find this post useful? We at notiz.dev write about our experiences developing Apps, Websites and APIs and develop Open Source tools. Your support would mean a lot to us 🙏. Receive a reward by sponsoring us on Patreon or start with a one-time donation on GitHub Sponsors.