Grow your CSS skills. Land your dream job.

Media Query Change Detection in JavaScript Through CSS Animations

Published by Guest Author

The following is a guest post by Alessandro Vendruscolo. Media queries are relevant to both CSS and JS. The need and desire to manage those in one place is real. There have been some clever ways to do this, like Jeremy Keith's Conditional CSS. But in that case, the onus is on you to test after window state changes. You can get a true listener with MediaQueryList, but then you're maintaining the media queries in both places again. Ah well, I'll let Alessandro explain.

Media queries were first introduced more than twelve years ago (yes, the first draft dates back to 4 April 2001!) and were introduced to limit the scope of a style sheet:

A media query consists of a media type and one or more expressions to limit the scope of a certain style sheet. […] By using media queries, content presentations can be tailored to a range of devices without changing the content itself.

With media queries in CSS we can selectively use styles depending on, for example, the width of browser screen:

@media screen and (min-width: 960px) {
  body {
    padding: 50px;
  }
}

In the example above, if your browser screen is wider than or equal to 960px, the body will have 50px of padding.

DOM events triggered from CSS animations

A simple click event:

document.querySelector('a.button').addEventListener('click', function(event) {
  // do something
});

Nothing fancy here, just standard DOM programming. The click event, among many other events, are generated when the user interacts with the page: clicks, moves the cursor, scrolls the page and so on.

All of these events are related to the DOM, and generates as a consequence of user's interactions. There are also some events which are specifically related to CSS animations: animationStart, animationEnd (and their transition cousins, transitionStart, transitionEnd).

If we insert a new element onto the page and it has an animation, we know that CSS animation will start as soon as it is inserted. So if we watch for the animationstart event of that animation, we can know when it was inserted.

@keyframes nodeInserted {
  from { clip: rect(1px, auto, auto, auto); }
  to { clip: rect(0px, auto, auto, auto); }
}
.an-element {
  animation-duration: 0.001s;
  animation-name: nodeInserted;
}
document.addEventListener("animationstart", function (event) {
  if (event.animationName == "nodeInserted") {
    // an element with class an-element has been inserted in the DOM
  }
}, false);

Thanks to David Walsh, Daniel Buchner, and Omar Ismail for blogging about this around a year ago.

Putting together media queries and animations

Perhaps you can see where this is going. If we trigger the animation when a media query changes (rather than on node insertion), we can also detect when that media query change happened in our JavaScript.

body {
  animation-duration: 0.001s;
}
@media screen and (min-width: 1000px) {
  body {
    animation-name: min-width-1000px;
  }
}
@media screen and (min-width: 700px) {
  body {
    animation-name: min-width-700px;
  }
}

@keyframes min-width-700px {
  from { clip: rect(1px, auto, auto, auto); }
  to { clip: rect(0px, auto, auto, auto); }
}

@keyframes min-width-1000px {
  from { clip: rect(1px, auto, auto, auto); }
  to { clip: rect(0px, auto, auto, auto); }
}
document.addEventListener(animationEnd, dispatchEvent, false);

// check the animation name and operate accordingly
function dispatchEvent(event) {
  if (event.animationName === 'min-width-700px') {
    document.body.innerHTML = 'Min width is 700px';
  } else if (event.animationName === 'min-width-1000px') {
    document.body.innerHTML = 'Min width is 1000px';
  }
}

Editor's note: choose your names wisely.

Demo on CodePen:


We have the same simple animation that doesn't actually do anything, but this time, the animation is inside an @media block.

If the window becomes wider than 700px, the min-width-700px animation will trigger, and our dispatchEvent will get this information. Think of it like a callback. We can then adapt our page by showing new widgets, destroying others, or whatever else you need to do to adapt to the new screen size.

The cool thing is that we don't need to use the window resize event, which can be slow and fire too many times (throttling is normally needed). The animationstart event will trigger as soon as the Media query matches, even if we didn't resize the window. If we then resize the window, the event will fire just when the media query matches.

The right way to do these things

Now we've talked about how to get JavaScript callbacks of window width changes, without relying the resize event.

But I wouldn't suggest you to put this in production code. It works, but the right way to do this should be to use the matchMedia method, which returns a MediaQueryList. MediaQueryList objects can have listeners, and thus we can use the addListener method to get notified when a Media query matches or not.

Enquire.js has recently reached version 2, and wraps the matchMedia method, which gives us a clean and robust wrapper.

Comments

  1. May I ask why you do not recommend using this in production code…I like Enquire.js but its reliance on MatchMedia I am guessing means it may not work in IE9…

    IE 10 supports matchMedia but this browser is quite new in IE terms and it could be a while before a lot of Windows users upgrade to IE 10…

    Your solution I am guessing would work across most modern browsers…So why not use it in production code?

    • This relies on CSS animations, which are not supported on IE9. Plus, MatchMedia has a polyfill, so you can still use enquire.js on older browsers.

      I wouldn’t use in production just for the sake of using the right tool to do the right job. This hack works, but it’s still an hack!
      It requires you to put an animation just to detect a media query match. MatchMedia is the right thing to use for this kind of job.

    • Enquire author here :)

      Alessandro is right, you’re better off using matchMedia as it was designed exactly for this purpose. If you provide a polyfill for less capable browsers you will have no problems.

      The traditional matchMedia polyfill by Scott Jehl & Paul Irish allows browsers that support CSS3 media queries, but do not have the JS matchMedia API to work with matchMedia perfectly fine. However, this does mean it will only work for browsers with CSS3 media query support – IE9 and Opera 12.0 fall into this category.

      If you require more browser support, consider a polyfill with deeper support, which would basically emulate CSS3 media queries in their entirety. A great example of this is David Knight’s media-match. It works all the way back to IE6! Of course this comes at a cost, in terms of file size, as this polyfill does a lot more than the previous polyfill.

      So there you go, decide on browser support, pick an appropriate polyfill to achieve that, and away you go!

  2. This is pretty awesome and all, but can someone offer a real world instance where this would be practical so I can justify spending a few hours to really wrap my head around it? Thanks!

  3. Neat tricks. I’ve rather recently released mqa.js (media query aliases) for this purpose. It plays nice with polyfills so IE9 with polyfill works great. The reason for the project is rather maintenance-focused. You don’t want to spread out the same media query throughout your CSS and JS files, mqa.js mitigates that by using a custom ID-selector in the media query, parses that and triggers events using the alias in JavaScript. You can see more over at my GitHub page and a blog post explaining it a bit more.

    Thanks!

  4. MaxArt

    Interesting insight and good idea.
    After the node DOM insertion trick came out, I thought about how it can be used – class changes is the first thing that came to my mind.
    What prevented me to exploit it is that it’s not supported by IE9.

  5. I had never seen Jeremy Keith’s Conditional CSS article, but the solution I came up with, and blogged about last week is basically the same. In my case I just apply a color to an element that is hidden, and determine the “breakpoint level” easily. By using hex colors like #000000, #010101, #020202, etc., my breakpoints are 0, 1, 2, etc.

  6. Nathan

    I was wondering about this a short while ago… thanks for the break-down!

  7. Gregory

    There’s something I don’t get. If it’s not something that has its place in production code, why evangelize it in the first place?

  8. M
    Permalink to comment#

    Great idea !

    Well, animations events are not well supported. I suggest you use transition events that have a much better support. It’s also better to use transition because you don’t need to create a fake animation, you can reuse the property you use in your media query for the transition.

This comment thread is closed. If you have important information to share, you can always contact me.

*May or may not contain any actual "CSS" or "Tricks".