Responsive JavaScript and the matchMedia Method

In Brief
Use window.matchMedia(query) when JavaScript behaviour must follow a media query: read .matches for the initial state, then listen for change only if later changes matter. Prefer CSS when the difference is purely presentational. Keep the query's listener lifecycle explicit and remove it when its owner is finished, just as you would with any other event listener.
We've all worked with JavaScript functions that should do different things at different screen sizes, on different devices or in different browsers, or based on certain user preferences.
And there are many established ways of handling that in JavaScript, although it always feels a little hacky, and often comes at the cost of additional weight on the client, resulting in a more 'janky' experience.
A common approach is to read window.innerWidth on page load and on each resize event. When we only need to know whether a breakpoint matches, matchMedia() gives us a more direct expression of that condition:
The matchMedia() method
This was a pleasant discovery, and I don't mind admitting that I might have come across this one a little later than some.
The often‑overlooked matchMedia() JavaScript method is a handy‑dandy way of using CSS media query syntax (e.g., (min-width: 1024px)) directly within JavaScript. It also means that we can use the same CSS syntax in our JavaScript to detect any of the other various browser or user preferences that media types or media features encompass.
This is clearly a much neater and more concise way of handling different visitor types, characteristics, preferences, or parameters.
A media‑query listener runs when the query's match state changes, rather than on every resize. That can avoid unnecessary callbacks, but it is not a measured guarantee that the whole application will be faster. Older browsers may support matchMedia() without supporting the modern MediaQueryList.addEventListener() API, so check both parts.
At this point, I want to quickly stress just how powerful matchMedia can be for detecting media features. Although I only intend to focus on screen sizes today, just remember that anything you can do with a media query in CSS, you can also do with matchMedia.
Setting Up the Media Query
Getting matchMedia() up and running is super simple; it's literally three lines of code (four if you really want to count that trailing bracket):
const isDesktop = window.matchMedia('(min-width: 1024px)');
if (isDesktop.matches) {
console.log('viewport at least 1024px!');
}So, what's that .matches property? matchMedia() returns a MediaQueryList object (you can check out some more in‑depth docs on that type of object here), which has its own methods and properties. .matches is one of those properties and contains a boolean value of either true or false, depending on whether the media query is matched.
However, to get this media query working the same way that a regular CSS query would work (i.e actually responsively, on window size change), we'll need to use one of the methods that MediaQueryList exposes to us.
The change event
const isDesktop = window.matchMedia('(min-width: 1024px)');
isDesktop.addEventListener('change', e => {
if (e.matches) {
console.log('viewport at least 1024px!');
}
});The listener runs when the query changes between matching and not matching. A resize that stays on the same side of the breakpoint does not trigger it. Read .matches from the event to choose the appropriate behaviour.
Registering the listener does not call it for the initial state. We can already read .matches from the MediaQueryList, so call the same handler once after registration:
If we extract out our resize‑handling function, then triggering it could look like the code below, although you would almost certainly want to make sure the page is ready before triggering, maybe inside a useEffect() hook, or ‑ if you're still using jQuery ‑ inside of $(document).ready() 🤮.
const isDesktop = window.matchMedia('(min-width: 1024px)');
const handleResize = e => {
if (e.matches) {
console.log('viewport at least 1024px!');
}
};
// handles our media query as/when it changes
isDesktop.addEventListener('change', handleResize);
// instantiates the media query at load-time
handleResize(isDesktop);
// use this during teardown
// isDesktop.removeEventListener('change', handleResize);A note on the .addListener() method
Previously, you would have achieved the same as the above by using the .addListener() method, and you will likely find plenty of code examples that use that instead of .addEventListener(). The example above would then have looked something more like this:
const isDesktop = window.matchMedia('(min-width: 1024px)');
const handleResize = e => {
if (e.matches) {
console.log('viewport at least 1024px!');
}
};
isDesktop.addListener(handleResize);
handleResize(isDesktop);
// During teardown:
// isDesktop.removeListener(handleResize);addListener() is deprecated, but it remains useful when supporting older browsers without the modern event API. Pair it with removeListener() during teardown. For modern browsers, use addEventListener("change", handler) and remove that same handler with removeEventListener("change", handler).
The Wrap‑up
matchMedia() is a useful way to express JavaScript behaviour that follows a media query. Keep the query and its initial state together, and make listener cleanup part of the component or feature that owns it.
It opens the door for more accessible functionality by exposing a simple way to use CSS media queries like prefers-reduced-motion or prefers-contrast, and is widely supported by browsers. It's definitely a method that should get a little more love!