Media Query Change Detection in JavaScript Through CSS Animations

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.