One of the more recent features available in browsers is the ability to do CSS Media Queries based on user theme & accessibility settings in the operating system - for example using @media (prefers-color-scheme: dark)
(see 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 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 using RxJS 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, an object that contains an onabort
callback - the parent of the signal is
a 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.
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.
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.
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 provides some more example of how to do this in full.
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
RxJS Ninja - is a collection of over 130 operators for working with various types of data such as arrays, numbers and 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.
Photo by Lubo Minar on Unsplash
Top comments (0)