A lesson in Sass Maps: make Sass media queries talk to JavaScript

Get media queries to JavaScript from the Sass file.

Here’s a little experiment of mine. Remember the technique, from few years back, where you would stick the name of a breakpoint into a :before or :after pseudo element in body tag, like this:

body {
    @include bp(m) {
        &:after {
            content: 'm';
        }
    }

    @include bp(l) {
        &:after {
            content: 'l';
        }
    }

    @include bp(xl) {
        &:after {
            content: 'xl';
        }
    }

    &:after {
        content: 's';
        display: none;
    }
}

And then read it with JavaScript using getComputedStyle:

var size = window.getComputedStyle(document.body, ':after').getPropertyValue('content');
// size equals to your current breakpoint now

I “improved” it a little, and made a Sass mixin to encapsulate it better. The mixin uses Sass maps, with the map value being a Sass list. More on the JavaScript bit later in the article.

The mixin

This mixin sticks the breakpoints name into :before and the breakpoints size into :after. I have no idea if the size is needed for anything, I haven’t needed it ever, but there might be a use case for it, I’ll leave that up to you.

This is actually a set of mixins, it needs a breakpoint mixin as it companion. Read the comments in the code for more details:

// ----
// Media queries
// ----
$breakpoints: (
    "s":   (min-width, 48em),
    "m":   (min-width, 50em),
    "l":   (min-width, 64em),
    "xl":  (min-width, 90em),
    "xxl": (min-width, 105em)
) !default;

// Make the breakpoints
@mixin bp($breakpoint) {
    $query: map-get($breakpoints, $breakpoint);

    @if $query != null {
        @media (#{nth($query, 1)}: #{nth($query, 2)}) {
            @content;
        }
    } @else {
        @warn "Unfortunately, no value could be retrieved from #{$breakpoint}. "
        + "Please make sure it is defined in `$breakpoints` map.";
    }
}

// ----
// Here's the sauce, this jugs the BPs name (key) into an `:after` and the
// value to `:before` and `:after` pseudo elements, we can read that with
// JavaScript.
// ----
@mixin bp2js($default) {

    // One of the BPs has to be the default, meaning that it's on when
    // there's no BP affecting. In the min breakpoint mobile first approach,
    // the default would be the smallest BP.
    &:before {
        $foo: map-get($breakpoints, $default);
        display: none;
    }

    &:after {
        content: '#{$default}';
        display: none;
    }

    @each $point, $dim in $breakpoints {
        @if $default != $point {
            @include bp(#{$point}) {
                &:after {
                    content: '#{$point}';
                }

                &:before {
                    content: '#{nth($dim, 2)}';
                }
            }
        }
    }

    @content;
}

Demo:

Play with this gist on SassMeister.

JavaScript to read the values

Here’s the script wrapped into an AMD module:

/**
 * mq2js.js
 */
'use strict';
define(function() {
    var getValues, value;
    getValues = function(el) {
        value = window.getComputedStyle(document.body, ':' + el)
            .getPropertyValue('content');
        return value.replace(/(\'|\")/g, '');
    };
    return {
        name: getValues('after'),
        size: getValues('before')
    };
});

It returns an object called bp with with two keys:

  • name — the breakpoint name
  • size — the breakpoint width

Usage

Here’s an example that prints them out to the console:

define(function(require) {

    // Require the module
    require('./mq2js')

    // Define environment. Ideally you'd check
    // this programatically somehow
    var isDev = true;

    // Make a little helper function
    var bpShower = function() {
        console.log('BP name: ' + bp.point + 'n BP width: ' + bp.width);
    }

    // If on dev, run it
    if (isDev === true) {
        bpShower();
        // Run also on resize
        window.addEventListener('resize', bpShower);
    }
}

Another use case for this is to load assets conditionally depending on the screen size:

define(function(require) {

    // Require the module
    require('./media-queries')

    // Check the that we're on the large breakpoint
    if (bp.point === 'xxl') {
        // Load an image gallery, for example
        $("#result").load("ajax/images-template.html", function() {
            // This is a callback for the load,
            // init lazyload here or something else...
        });
    }
}

Thoughts and conclusions

This is very light and library-free method of doing this, albeit not very advanced nor sophisticated. There’s libraries like enquire.js that can do tons of amazing stuff for you! Enquire.js is also dependency free, get to know it here, it’s quite alright.

Club-Mate, the beverage → club-mate.fi