Matching multiple CSS media queries
using window.matchMedia()

Created: Feb 11th, 2015

A common question that gets asked is how to use window.matchMedia()
to react to multiple CSS media queries. In the tutorial CSS media query matching in
JavaScript, we get a quick overview of window.matchMedia()
and using it to respond to a single CSS media query change:

Here we are monitoring just one CSS media query using window.matchMedia(), namely, "screen and (max-width: 800px)",
and reacting whenever the browser crosses between that threshold.

Responding to multiple CSS media queries

To respond to more than one CSS media query using window.matchMedia(), we basically just repeat the above blueprint for
one media query multiple times. To streamline the code, we can use an array
to store all of our window.matchMedia() queries first, then use
a for loop to invoke a single function that handles all of the
queries. Lets see this now:

Click here to
see a live example of the above- as you resize the browser window
horizontally and vertically, different Boolean values are shown reflecting
which media queries are currently matched.

We now have the basic pattern for hooking up multiple media
queries with window.matchMedia(), though like many things, the
devil is in the details. When we have a single handler function that
responds to all of our media queries, it means this function will be invoked
multiple times. That in itself is not a problem, and is by design actually.
In the example above, the handler function window.matchMedia()
is hooked up to 3 different window.matchMedia() queries, and is
hence called the following number of times:

Three times when the page first loads, one time each to
deal with each query that may be matched when the page first loads

Once every time the threshold for one of the entered
queries are met. If the user resizes the browser from 900px to 860px, then
to 700px, the query "(max-width: 860px)" is triggered once,
when the browser crosses the 860px threshold. Resizing the window back to
900px triggers the same query again.

While calling our handler function multiple times is by
design in order to handle all of our window.matchMedia()
queries, what you may not want is to run everything inside this function
during each invocation, for the sake of efficiency at the very least. In the
example above, when a match for "(max-width: 860px)" is made,
all 3 lines inside the function are run, instead of just the line that sets
the "#match1" element to a corresponding Boolean value. This can be
avoided by selectively running code based on the media query that triggered
the function, which is what we'll look at next.

- Finding out which window.matchMedia() query
triggered the handler function

With a single function handling all window.matchMedia()
query matches, it's useful- if not necessary- sometimes to figure out which
exact query triggered the function. This is different from simply
determining if a query was successfully matched, which we can easily figure
out using the matches property of each query stored in our
array:

To figure out which window.matchMedia() query
actually triggered the handler function, we need to go beyond just examining
the matches property, and look to the media
property of the incoming MediaQueryList
object as well, which returns a serialized string of the triggering query
list. In the handler function, the MediaQueryList
object is passed as the first parameter of the function, or in this case the
parameter in red:

To complicate things slightly, the return value for the
media property of MediaQueryList
object is slightly different between non IE (as of IE11) and IE browsers.
Given the below window.matchMedia() query for example:

var mql = window.matchMedia("(max-width: 860px)")

In non IE browsers, mql.media returns exactly "(max-width:
860px)", while in IE, it returns "all and (max-width:860px)"
instead. So what IE returns differently is the following:

Adds a media of "all" in front of the
string in the absence of a media specified in the query

Removes any space between each property and property value,
so no space in "max-width:860px".

We can equalize these differences when probing the media
property with a little regular expressions. The following window.matchMedia() handler function selectively executes different code
based on which one of the window.matchMedia() queries list in our
mqls array was matched:

Examining the media property first inside your
handler function ensures only specific portions of the function body are
executed based on which media query triggered the handler. The result is similar
to defining separate functions for each of the window.matchMedia()
queries, with the benefit of a more manageable single function. It is not
however without drawbacks. A lot of times CSS media queries will overlap in
scope, so the code targeting one query will also test true for another. By
segmenting your function code based on the incoming window.matchMedia() query, each block becomes a mutually exclusive zone
unable to apply itself to another zone at the same time, which depending on the
set of media queries you're working with will be necessary. In that case, using
mql.matches instead as your primary logic switch is a better route,
even if it means the same code may be run more than once.

Example- reacting to a responsive layout

Lets see a more elaborate example now of using JavaScript to
react to a 3 column responsive layout, where the scope of one CSS media query
overlaps another, and how to handle that in our JavaScript handler function. The
following 3 column layout uses ordinary CSS media queries to change to a 2
column when the browser width is 840px or below, and when at 600px or below,
change to a single column instead. Here is the example page we'll be working
with first:

Resize the page past the 860px and 600px break points to see the
layout shift in structure. Right now we have static text in each of the columns
that shows the original widths of the columns- "180px, fixed, and
190px" respectively. We'll use JavaScript to dynamically change this text
at the 840px and 600px break points to reflect the changes in columns widths
accordingly. The result is the following:

Resize the new page past the 860px and 600px break points to see
the text update to reflect the current columns state. To accomplish this, our
JavaScript has to react to the same two media queries used in the page's CSS:

@media (max-width: 840px){}

@media (max-width: 600px){}

and account for the following 3 scenarios:

When the layout is 840px or below

When the layout is 600px is below

When the layout is neither 860px or below nor 600px or below
(non responsive)

The logic inside our handler function is set up so each portion
of the code is not mutually exclusive from one another- when the query "(max-width:
600px)" is matched for example, so is "(max-width: 840px)",
and possibly visa versa, so we shouldn't use an "else" statement
following each "if" statement to capture the "opposing" condition,
which can be one of many. Instead, simply execute our code progressively, and in
the end test for when neither queries are matched to detect when the layout is
neither 840px nor 600px wide.

Now, take a look at this line:

leftcolumn.innerHTML = "180px" //not redundant

inside the "if" clause matching when the layout is
840px or below. It might look redundant- after all, we're already setting "leftcolumn"
to display "180px" when the screen is wider than 840px, so going into 840px, why
repeat the same action? The reason is we also need to account for events that
happen in the opposite direction- that is, going from a narrow screen (ie: 640px
or less) to a wider screen (ie: 840px or more). At the 640px stage, "leftcolumn"s
text is replaced with "Fluid (Responsive layout triggered)". When the user
resizes the browser back to 840px or more, there needs to be code undo what was
done at the 640px stage.