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:
function maxwidth800action(mql){ if (mql.matches){ console.log("Your window is 800px or below") } else{ console.log("Your window is bigger than 800px") } } var mql = window.matchMedia("screen and (max-width: 800px)") maxwidth800action(mql) // call maxwidth800action() at run time mql.addListener(maxwidth800action) // call maxwidth800action() whenever media query is triggered
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:
var mqls = [ // list of window.matchMedia() queries window.matchMedia("(max-width: 860px)"), window.matchMedia("(max-width: 600px)"), window.matchMedia("(max-height: 500px)") ] function mediaqueryresponse(mql){ document.getElementById("match1").innerHTML = mqls[0].matches // width: 860px media match? document.getElementById("match2").innerHTML = mqls[1].matches // width: 600px media match? document.getElementById("match3").innerHTML = mqls[2].matches // height: 500px media match? } for (var i=0; i<mqls.length; i++){ // loop through queries mediaqueryresponse(mqls[i]) // call handler function explicitly at run time mqls[i].addListener(mediaqueryresponse) // call handler function whenever the media query is triggered }
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:
function mediaqueryresponse(mql){ if (mqls[0].matches){ // do something when width: 860px media query matches // do something } if (mqls[1].matches){ // do something when width: 600px media query matches // do something } if (mqls[2].matches){ // do something when height: 500px media query matches // do something } }
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:
function mediaqueryresponse(mql){ console.log(mql.media) // returns "(max-width: 860px)" for example }
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 "
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:
var mqls = [ // list of window.matchMedia() queries window.matchMedia("(max-width: 860px)"), window.matchMedia("(max-width: 600px)"), window.matchMedia("(max-height: 500px)") ] function mediaqueryresponse(mql){ if (/\(max-width:\s*860px\)/.test(mql.media)){ // when "(max-width: 860px)" query is triggered //do something. Probe mql.matches to see if query condition is actually met } else if (/\(max-width:\s*600px\)/.test(mql.media)){ // when "(max-width: 600px)" query is triggered //do something. Probe mql.matches to see if query condition is actually met } else if (/\(max-height:\s*500px\)/.test(mql.media)){ // when "(max-height: 500px)" query is triggered //do something. Probe mql.matches to see if query condition is actually met } }
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:
3 column responsive layout (with dynamic text)
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)
Here is the JavaScript in full:
var leftcolumn = document.getElementById("leftcolumn").getElementsByTagName("em")[0] var rightcolumn = document.getElementById("rightcolumn").getElementsByTagName("em")[0] var maincolumn = document.getElementById("contentcolumn").getElementsByTagName("em")[0] var mqls = [ window.matchMedia("(max-width: 840px)"), window.matchMedia("(max-width: 600px)") ] function mediaqueryresponse(mql){ if (mqls[0].matches){ // {max-width: 840px} query matched leftcolumn.innerHTML = "180px" //not redundant maincolumn.innerHTML = "Fluid (Responsive layout triggered)" rightcolumn.innerHTML = "Fluid (Responsive layout triggered)" } if (mqls[1].matches){ // {max-width: 600px} query matched leftcolumn.innerHTML = "Fluid (Responsive layout triggered)" } if (!mqls[0].matches && !mqls[1].matches){ // neither queries matched rightcolumn.innerHTML = "190px" leftcolumn.innerHTML = "180px" maincolumn.innerHTML = "Fixed" } } for (var i=0; i<mqls.length; i++){ mediaqueryresponse(mqls[i]) // call listener function explicitly at run time mqls[i].addListener(mediaqueryresponse) // attach listener function to listen in on state changes }
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.
End of Tutorial