nray.dev home page

Using Media Queries in JavaScript

Media queries in CSS are often used to change the styling or visibility of elements when the website is accessed with a particular device/environment or when the browser's viewport size or orientation changes. They are a vital part of any responsive web design.

You may sometimes want to execute JavaScript code when a media query condition has been met. For example, maybe you want to run code when the viewport width is greater or equal to 1000px. The resize event is often used in this situation:

window.addEventListener("resize", (e) => {
  if (window.innerWidth >= 1000) {
    // The viewport is greater or equal to 1000px.
  }
});

There are multiple problems with this approach, however:

  • It can be expensive. The event listener callback will be called each time the browser's window is resized. As a result, it can fire at an extremely high rate and make your website feel sluggish if your callback has expensive operations.
  • It can become complex. To mitigate performance issues and reduce the frequency that the code inside the event listener callback executes, a developer may use debouncing or throttling. In addition, if you only want the code in the callback to run when it crosses the breakpoint, you may have to introduce some state that tracks if the viewport has already crossed a particular breakpoint so that it doesn't fire multiple times.

There must be a better way to execute some JavaScript code when the viewport crosses a particular breakpoint or meets a media query condition. Luckily, JavaScript has your back!

Using matchMedia() in JavaScript

The matchMedia API is a performant and simple solution that should be considered when you want JavaScript code to execute when a media query status has changed.

// Set a media query in JavaScript.
const mediaQuery = matchMedia("(min-width: 1000px)");
// Listen to viewport width changes involving the media query above.
mediaQuery.addEventListener("change", (e) => {
  // Using `e.matches` is not prone to expensive style recalcs/layout
  // that checking properties like `window.innerWidth` might cause.
  if (e.matches) {
    // The viewport is >= 1000px wide.
  } else {
    // The viewport is < 1000px wide.
  }
});

Unlike the resize event handler callback, the callback passed to mediaQuery.addEventListener() in the example above will only fire when a change in media query status has occurred. Because of this, we don't have to worry about using throttling or debouncing or keeping track of whether the code has already been executed for a given viewport state.

Using addListener to support older browsers.

There is a caveat with this approach, though. Older browsers such as versions of Safari before version 14 implement an older and now deprecated API. They don't support mediaQuery.addEventListener() and will throw an error if used. Instead, they use mediaQuery.addListener() which has a slightly different call signature.

Because of the differences in browser support, it is imperative to use feature detection so that the widest range of browsers are supported:

const mediaQuery = matchMedia("(min-width: 1000px)");
 
const handler = (e) => {
  // Using `e.matches` is not prone to expensive style recalcs/layout
  // that checking properties like `window.innerWidth` might cause.
  if (e.matches) {
    // The viewport is >= 1000px wide.
  } else {
    // The viewport is < 1000px wide.
  }
};
 
// If the browser supports the `addEventListener` API, the following
// line will be truthy, so we use it. If not, it will be falsy, and we
// use the deprecated `addListener` API.
mediaQuery.addEventListener
  ? mediaQuery.addEventListener("change", handler)
  : mediaQuery.addListener(handler);

Running code on page load

The matchMedia API also supports querying the media query status. This can be important when setting up the page's initial state during page load, for example:

// Set a media query in JavaScript.
const mediaQuery = matchMedia("(min-width: 1000px)");
// Check media query status.
if (mediaQuery.matches) {
  // The viewport is >= 1000px wide.
} else {
  // The viewport is < 1000px wide.
}

Full solution

The full native JavaScript solution will often look something like the following:

// Set a media query in JavaScript.
const mediaQuery = matchMedia("(min-width: 1000px)");
 
// Listen to viewport width changes involving the media query above.
const handler = (e) => {
  // Using `e.matches` is not prone to expensive style recalcs/layout
  // that checking properties like `window.innerWidth` might cause.
  if (e.matches) {
    // The viewport is >= 1000px wide.
  } else {
    // The viewport is < 1000px wide.
  }
};
 
// If the browser supports the `addEventListener` API, the following
// line will be truthy, so we use it. If not, it will be falsy, and we
// use the deprecated `addListener` API.
mediaQuery.addEventListener
  ? mediaQuery.addEventListener("change", handler)
  : mediaQuery.addListener(handler);
 
// Check during page load.
handler(mediaQuery);

Using media queries in React

The matchMedia API can also be a useful hook in React. Here it is with typescript:

import { useState, useEffect } from "react";
 
/**
 * @param query The media query to match against.
 */
function useMediaQuery(query: string) {
  // Support isomorphic rendering by checking if `window` (clientside)
  // exists.  If not, then this code is probably being executed on the
  // backend.
  const mediaQuery =
    typeof window !== "undefined" ? matchMedia(query) : false;
  const [matches, setMatches] = useState(mediaQuery && mediaQuery.matches);
 
  useEffect(() => {
    if (!mediaQuery) {
      return;
    }
 
    const handler = (e: MediaQueryListEvent) => {
      /**
       * Using `e.matches` is more performant than checking properties
       * like `window.innerWidth` which are prone to causing expensive
       * style recalcs/layout operations.
       *
       * @see https://gist.github.com/paulirish/5d52fb081b3570c81e3a
       */
      if (e.matches) {
        // The viewport is >= 1000px wide.
        setMatches(true);
      } else {
        // The viewport is < 1000px wide.
        setMatches(false);
      }
    };
 
    // If the browser supports the `addEventListener` API, the
    // following line will be truthy so we use it. If not, it will be
    // falsy and we use the deprecated `addListener` API.
    mediaQuery.addEventListener
      ? mediaQuery.addEventListener("change", handler)
      : mediaQuery.addListener(handler);
 
    // Clean up event listener after this effect is no longer needed.
    return () =>
      mediaQuery.removeEventListener
        ? mediaQuery.removeEventListener("change", handler)
        : mediaQuery.removeListener(handler);
    // Don't change when the mediaQuery reference changes as we only
    // use that on the first render.  eslint-disable-next-line
    // react-hooks/exhaustive-deps
  }, [query]);
 
  return matches;
}
 
export default useMediaQuery;

Using the hook is then as simple as passing in the media query:

function Component() {
  const matches = useMatchMedia("(min-width: 1000px)");
 
  return (
    <div>
      {matches && ">= 1000px"}
      {!matches && "< 1000px"}
    </div>
  );
}

Conclusion

JavaScript's matchMedia API is a simple and performant way of detecting when a media query status change has occurred. Use it without the performance penalty of alternative solutions like listening to the resize event.

Become a page speed expert

Get web performance tips that improve page speed, user happiness, and business results 🚀

No spam. Unsubscribe anytime.