Create your own Dark Mode Detection Observable using RxJS and Media Queries
Tags: tutorial, rxjs, typescript, webdev
Published: 2021-01-27 21:14:17

---
title: Create your own Dark Mode Detection Observable using RxJS and Media Queries
published: true
description: Learn how to create a new RxJS Observable that tells you if a users browser or operating system is in Dark Mode and how to share that with your apps
tags: tutorial, rxjs, typescript, webdev
canonical_url: https://tane.dev/2021/01/create-your-own-dark-mode-detection-observable-using-rxjs-and-media-queries/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/qz15yzwsfg40ofzgfgb6.jpg
---

[Demo Link](https://stackblitz.com/edit/rxjs-media-query-observable)

One of the more recent features available in browsers is the ability to do [CSS Media Queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries) based on user theme & accessibility settings in the operating system - for example using `@media (prefers-color-scheme: dark)` (see [prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)) you can check if the user's OS theme is currently in Dark Mode and use this to set a websites theme accordingly.

The query is also available in JavaScript using the [window.matchMedia](https://developer.mozilla.org/en-us/docs/Web/API/Window/matchMedia) function - that returns a `MediaListQuery` that will allow us to do two things:

* The current value of the users setting via the `matches` boolean property
* Any future values by listening to its `changes` event and attaching an event lister function to it

Combining these, it's the perfect candidate to turn into a [fully reactive dark mode switcher](https://stackblitz.com/edit/rxjs-media-query-observable) using [RxJS](https://rxjs.dev) and Observables that will give us the users current setting.  If you're not familiar with Observables, they are a type of stream that emits values over time - consumers can subscribe to these Observables to get their values - this means we can use them to get values from long running functions or event emitters.

In the full demo you'll find an example page with light and dark mode set from your own OS settings. To see the full working example you need to change the setting (e.g. in OSX Dark Mode is under "General" settings) - also provided are a user toggle button, and a button to turn off and on the media query listener. The Observable in this example supports more than one `prefers-` type of query but in the tutorial below we'll build a much simpler `isDarkMode` Observable than the one provided in the demo, but the concept is the same.

## Creating a Dark Mode Observable

For our code we first need to create our Observable factory - this is the function that allows us to pass any required parameters for the implementation and returns an Observable which can then be subscribed to.

The Observable constructor takes a function - a callback any time there is a new subscription - this is where the implementation will live.

As soon as the subscription opens, we first check to see if `window.matchMedia` is available - it should be available in all modern browsers but is not available in environments like node (yay unit testing!) - so here we can throw an error.

The factory also accepts an optional [AbortSignal](https://developer.mozilla.org/en-us/docs/Web/API/AbortSignal), an object that contains an `onabort` callback - the parent of the signal is
a [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and using this we can externally signal our Observable to close all subscriptions and remove all event listeners.

The return value of the constructor is another function - the teardown logic - this is called when an RxJS subscription is ended, such as using `takeUntil` or `take(1)` - here we also ensure that all subscriptions and event listeners are closed.

```ts
import { Observable } from 'rxjs';

export function isDarkMode(signal?: AbortSignal): Observable<boolean> {
  return new Observable<boolean>(subscriber => {

    if (!window.matchMedia) {
      subscriber.error(new Error('No windows Media Match available'));
    }

    if (signal) {
      signal.onabort = () => {
        !subscriber.closed && subscriber.complete()
      }
    }

    return () => {
      !subscriber.closed && subscriber.complete()
    }
  });
}
```

### Adding the Media Query

The main implementation of our Observable is to create our `MediaListQuery` and use it to emit values to any subscribers. On creation, contain a `matches` value of `true` or `false` which can be immediately be passed to `subscriber.next`.

We also need to bind a listener using to the `change` event of the query. As we also need to remove this later create an internal private function for the event handler - this will also call `subscriber.next` each time there is a detected change.

Also casting the event to a `MediaQueryListEvent` ensures TypeScript recognises it has the `matches` property which contains our value.

```ts
function emitValue(event: Event) {
  subscriber.next((event as MediaQueryListEvent).matches);
}

const mediaListQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaListQuery.addEventListener('change', emitValue);
subscriber.next(mediaListQuery.matches);
```

### Cleaning up handlers and subscriptions

Already we can start to use the new Observable, but we also need to make sure that we:

* End any subscriptions to the Observable when either the `AbortSignal` fires or RxJS unsubscribes from it
* Remove any event listeners in the DOM for the `change` event

With a slight bit of refactoring we have our final Observable factory below - in both the `signal.onabort` and the
Observable teardown logic we remove the event listener - the API for this requires you pass the function implementation
from our private function.

```ts
import { Observable } from 'rxjs';

export function isDarkMode(signal?: AbortSignal): Observable<boolean> {
  return new Observable<boolean>(subscriber => {

    if (!window.matchMedia) {
      subscriber.error(new Error('No windows Media Match available'));
    }

    function emitValue(event: Event) {
      subscriber.next((event as MediaQueryListEvent).matches);
    }

    const mediaListQuery = window.matchMedia('(prefers-color-scheme: dark)');

    if (signal) {
      signal.onabort = () => {
        mediaListQuery.removeEventListener('change', emitValue)
        !subscriber.closed && subscriber.complete()
      }
    }

    mediaListQuery.addEventListener('change', emitValue);
    subscriber.next(mediaListQuery.matches);

    return () => {
      mediaListQuery.removeEventListener('change', emitValue);
      !subscriber.closed && subscriber.complete()
    }
  })
}
```

### Finishing up

Now we have a fully working reactive Observable for a Dark Mode media query, this can be used in any application or website to check the users theme setting. The [demo] (https://stackblitz.com/edit/rxjs-media-query-observable) provides some more example of how to do this in full.

```ts
isDarkMode().pipe(
  tap(value => {
    body.classList.removeClass(value ? 'light' : 'dark');
    body.classList.addClass(value ? 'dark' : 'light');
  })
).subscribe()
```

This tutorial is just one small example of the kind of things that can be done with RxJS - any API that can emit values over time can be turned into Observables.

## A collection of pre-built operators and Observables for your projects

![The RxJS Logo, a Ninja jumping over a moon](https://raw.githubusercontent.com/rxjs-ninja/rxjs-ninja/main/assets/logo.png)

[RxJS Ninja](https://rxjs.ninja) - is a collection of over 130 operators for working with various types of data such as [arrays](https://rxjs.ninja/modules/array.html), [numbers] (https://rxjs.ninja/modules/number.html) and [streams](https://rxjs.ninja/modules/utility.html#Streams) allowing for modifying, filtering and querying the data.

Still in active development, you might find useful operators that provide clearer intent for your RxJS code.

You can check out the source code on [GitHub](https://github.com/rxjs-ninja/rxjs-ninja).

<span>Photo by <a href="https://unsplash.com/@bubo?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Lubo Minar</a> on <a href="https://unsplash.com/s/photos/dark-mode?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>