Categories:

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 "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:

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:

3 column responsive layout

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