DEV Community

Karl Castillo
Karl Castillo

Posted on

Media Queries in JS

In these modern times, your web applications can be viewed in a variety of screen sizes -- from small screen phones to large 4k monitors. Luckily CSS allows us to add certain stylings depending on many variables using media queries. Sometimes using media queries isn't enough to achieve the goal. This is where matchMedia could help.

matchMedia is a method provided by window that can determine whether the given media query matches the current state of the browser.

matchMedia

matchMedia accepts a media query as a string and returns a MediaQueryList which can be used to check if the current state of the browser matches the given media query.

const mediaQueryList = window.matchMedia("only screen and (max-width: 600px)");

if (mediaQueryList.matches) {
  console.log("Matches");
} else {
  console.log("Does not match");
}
Enter fullscreen mode Exit fullscreen mode

Keeping track of changes

We can keep track of these changes by listening for a change event.

const callback = (event) => {
  if (event.matches) {
    console.log("Matches");
  } else {
    console.log("Does not match");
  }
}

mediaQueryList.addEventListener("change", callback);
mediaQueryList.removeEventListener("change", callback);
Enter fullscreen mode Exit fullscreen mode

If you need to support older browsers, you can use addListener and removeListener respectively but do remember that those methods are deprecated.

mediaQueryList.addListener(callback);
mediaQueryList.removeListener(callback);
Enter fullscreen mode Exit fullscreen mode

useMediaQuery

This technology can also be transferred to a reusable React hook. The hook will accept a media query and a callback function for when changes occur.

const useMediaQuery = (query, callback) => {
  const [isMatchingQuery, setIsMatchingQuery] = useState(false);

  useEffect(() => {
    const mediaQueryList = window.matchMedia(query);
    const onMediaQueryUpdate = (e) => {
      setIsMatching(e.matches);

      if(callback) {
        callback(e);
      }
    };

    // Set whether the browser initially matches the query
    setIsMatchingQuery(mediaQueryList.matches);

    mediaQueryList.addEventListener("change", onMediaQueryUpdate);
    return () => {
      mediaQueryList.removeEventListener("change", onMediaQueryUpdate);
    }
  }, [query, callback, setIsMatchingQuery]);

  return { isMatchingQuery };
}
Enter fullscreen mode Exit fullscreen mode

If you're already using matchMedia in your project, how are you using it? If you're using a different framework, how would you incorporate matchMedia into that framework?

Discussion (3)

Collapse
darrylnoakes profile image
Darryl Noakes • Edited

I currently use it for a bunch of things, with automatically detecting user theme preference being my most common and handy use.

With Vue, it's as simple as making the callback modify a reactive variable.
You can also easily make a "composable", which is very similar to your React example.

Based partly on the VueUse library's one:

useMediaQuery.ts

import { onBeforeUnmount } from "vue";

export useMediaQuery(query: string) {
  // Create the media query
  const mediaQuery = window.matchMedia(query);
  // Create a ref to hold the state of the match,
  // initialized with the current value of `matches`.
  // This is what we will return.
  const matches = ref(mediaQuery.matches);

  // Create a handler to update the value of the ref.
  const handler = (event: MediaQueryListEvent) => {
    matches.value = event.matches;
  }

  // Attach the handler to the media query.
  if ("addEventListener" in mediaQuery)
    mediaQuery.addEventListener("change", handler);
  else
    mediaQuery.addListener(handler);

  // Attach a callback that removes the event listener when the component is unmounted.
  onBeforeUnmount(() => {
    if ("removeEventListener" in mediaQuery)
      mediaQuery.removeEventListener("change", handler);
    else
      mediaQuery.removeListener(handler);
  });

  return matches;
}
Enter fullscreen mode Exit fullscreen mode

AwesomeComponent.vue

...

<script setup>
import { watch, computed } from "vue";

import { useMediaQuery } from "@/composables/useMediaQuery";

// We can use this ref like any other,
// to do cool things like watching it or making computed refs.
const { matches } = useMediaQuery("(prefers-color-scheme: dark)");

// Watch the ref and change the theme based on the user's preference.
watch(matches, (value) => {
  // enable/disable dark mode based on `value`.
});

// We can use this `color` ref in the template, and it will automatically update.
const color = computed(() => {
  return matches ? "dark" : "primary";
});
</script>

...
Enter fullscreen mode Exit fullscreen mode

You do not really need to allow a callback to be passed in, as you can simply watch the returned ref.
Maybe the timing of reactivity updates would make it necessary, so that the callback gets called immediately when the media query changes, but I think that would be an extreme edge case.

useEventListener is also a common composable.

useEventListener.ts

import { isRef, unref, watch, onMounted, onBeforeUnmount } from "vue";

export function useEventListener(
  // the target could be reactive ref which adds flexibility
  target: Ref<EventTarget | null> | EventTarget,
  event: string,
  handler: (e: Event) => any
) {
  // If it is a ref, use a watcher that removes the event listener
  // from the previous target and attaches it to the new target.
  let stopWatcher = () => {};
  if (isRef(target)) {
    stopWatcher = watch(target,
      (value, oldValue) => {
        oldValue?.removeEventListener(event, handler);
        value?.addEventListener(event, handler);
      },
      { immediate: true }, // Run the callback immediately.
    );
  } else {
    // Else use the mounted hook
    onMounted(() => {
      target.addEventListener(event, handler);
    });
  }

  // Create a function that cleans up the event listener.
  const stop = () => {
    stopWatcher();
    unref(target)?.removeEventListener(event, handler);
  };

  // Clean it up when the component is being unmounted.
  onBeforeUnmount(stop);

  // Return the function so that the user can remove the event listener if they want to.
  return stop;
}
Enter fullscreen mode Exit fullscreen mode

Using this, we can rewrite useMediaQuery.

useMediaQuery.ts

import { useEventListener } from "@/composables/useEventListener";

export useMediaQuery(query: string) {
  // Create the media query
  const mediaQuery = window.matchMedia(query);
  // Create a ref to hold the state of the match,
  // initialized with the current value of `matches`.
  // This is what we will return.
  const matches = ref(mediaQuery.matches);

  // Create a handler to update the value of the ref.
  const handler = (event: MediaQueryListEvent) => {
    matches.value = event.matches;
  }

  // Use the composable to handle the event listener.
  useEventListener(mediaQuery, "change", handler);

  return matches;
}
Enter fullscreen mode Exit fullscreen mode

Note: this will no longer work with browsers that only support addListener and removeListener and not addEventListener and removeEventListener.

We could also make this optionally take a ref to a string instead of just a string, and update the media query when it is changed, like what happens with useEventListener. Or we could simplify useEventListener :).
But for useEventListener, having a reactive target makes sense, as this is useful in many cases, while useMediaQuery doesn't really need that functionality.

Collapse
prakh_r profile image
Prakhar Yadav

I haven't used matchMedia yet but it's a good tool to have in my arsenal of JS tricks. It can be used in my accessibility efforts to conform to WCAG.

Collapse
tim012432 profile image
Timo

Very useful. Thank you