Another approach to media queries with the power of Sass

When you've been working developing websites and layouts as long as I've been, you learn one thing, having dynamic not defined breakpoints and media queries is not a good idea, why?, because they give you more problems than they solve... take a look at the following code for instance:

.wrapper {
    margin: 0 -15px;
}

@media screen and (min-width: 420px) and (max-width: 835px) {
    .wrapper {
        margin: 0 -5px;
    }
}

@media screen and (min-width: 836px) and (max-width: 1130px) {
    .wrapper {
        margin: 0;
    }
}

Oh dear...

The Problem

There are many bad things going on in the example above:

  1. It has hacky/ackward breakpoints and media queries.
  2. It's not clear what screen or devices each media query is covering.
  3. It generates inconsistencies and issues that are hard to fix and often require a hacky solution.
  4. Many other things... (Sorry for this painful example...)

A step in the good direction

If you are as terrified as I am of finding code similar to the one above in a code base, it means that you're good and that you have had enough of that pain in your life as a developer (which means that it's time to move on), the first thing you do is think about how to organize your media queries?, what standard to use?, if you're developing a personal project you may have googled "what are the most important breakpoints that a website should cover?", (not that I did this in the past, winking face). The answer to these questions aren't always straightforward, it really depends on what you are building and on identifying which are the most important sources of traffic/income for your site/business. A couple of the most popular approches are to divide your media queries by screen sizes:

  • Extra small, small, medium, large.

or a combination of the most popular devices:

  • Phone, Tablet, Desktop, Large Desktop...

It's ok to choose any approach that suits you as long as it's consistent and scales, in Sass we could assign values to these combinations by using variables, something like:

$xs: 480px;
$sm: 768px;
$md: 992px;
$lg: 1200px;

or

$phone: 480px;
$tablet: 768px;
$desktop: 992px;
$large-desktop: 1200px;

This isn't bad, it allows us to create more verbose and consistent media queries such as:

@media screen and (min-width: $phone) and (max-width: $tablet - 1px) { ... }

which is ok, notice how we're substracting 1px from max-width: $tablet, the reason behind it is simple, if we use min-width: $tablet and we do not substract the 1px from the max-width one of the media queries will overlap because both are sharing one breakpoint (768px).

As I said, this approach is good but there is one thing, you'll still need to write all the media query statement for each case, including the -1px.

A Better approach to media queries with the power of SASS and mixins

Now that we know and understand the basics of good media queries, allow me to introduce you to a very powerful and flexible approach, we're going to start by showing the implementation, it looks a little complicated but believe me, using it is as easy as pie (if you want to start by reading the explanation first you can skip this block of code and come back later):

$devices: (
    phone: 480px,
    tablet: 768px,
    desktop: 992px,
    large-desktop: 1200px
);

@mixin min-device($device: map-get($devices, 'phone')) {
    @if map-has-key($devices, $device) {
        @media screen and (min-width: map-get($devices, $device)) {
            @content;
        }
    }
}

@mixin max-device($device: map-get($devices, 'phone')) {
    @if map-has-key($devices, $device) {
        @media screen and (max-width: map-get($devices, $device) - 1) {
            @content;
        }
    }
}

@mixin only-device($device: map-get($devices, 'phone')) {
    @if map-has-key($devices, $device) {
        $devices-length: length($devices);
        $map-list: map-keys($devices);

        @if index($map-list, $device) == $devices-length {
            @include min-device($device) {
                @content;
            }
        } @else {
            $next-device-index: index($map-list, $device) + 1;
            $next-device-key: nth($map-list, $next-device-index);

            @media screen and (min-width: map-get($devices, $device)) and (max-width: map-get($devices, $next-device-key) - 1) {
                @content;
            }
        }
    }
}

@mixin between-devices(
    $min-device: map-get($devices, 'phone'),
    $max-device: map-get($devices, 'tablet')
) {
    @if map-has-key($devices, $min-device) and
        map-has-key($devices, $max-device)
    {
        @media screen and (min-width: map-get($devices, $min-device)) and (max-width: map-get($devices, $max-device) - 1) {
            @content;
        }
    }
}

This approach will allow us to express media queries and breakpoints using a very simple and verbose approach, here is how you'd use it:

/* Apply to devices with a higher screen than phone */
@include min-device("phone") { ... }

/* Apply to devices with screen sizes smaller than tablet */
@include max-device("tablet") { ... }

/* Apply to only desktop devices */
@include only-device("desktop") { ... }

/* Apply to devices in a range, in this case, between tablet and large-desktop */
@include between-devices("tablet", "large-desktop") { ... }

No need to substract values from breakpoints everytime, no need to write long statements, no unnecessary redundancies, just an easy, understandable and smooth syntax, BOOM!

Explaining the code

Alright, here comes the fun part, it's time to explain the code piece by piece:

$devices: (
    phone: 480px,
    tablet: 768px,
    desktop: 992px,
    large-desktop: 1200px
);

First, we're using the power of Sass maps to store our values instead of variables, it's very similar to the variables example, the reason why we're using maps here is to make validations and execute processes more easily , we'll see more about it in the next piece of code.

@mixin min-device($device: map-get($devices, 'phone')) {
    @if map-has-key($devices, $device) {
        @media screen and (min-width: map-get($devices, $device)) {
            @content;
        }
    }
}

Here we're creating a mixin as a min-width handler, we're taking a parameter and validating that the parameter exists in the $devices map created at the beginning, we're using the very convenient map-has-key function to do this (one of the reasons why I used maps), then we're basically adding the content inside the media query with @content, we're doing the same for the max-device, only-device, between-devices mixins, the only real differences between them are:

  • max-device: substract 1px from the breakpoint passed to avoid the overlapping of properties.
  • only-device: limits the media query in a range where the parameter passed is defined as the min-width and the max-width is the next key that follows in the map structure, example:
@include only-device("phone") { ... }.

is the same as:

@media screen and (min-width: $phone) and (max-width: $tablet - 1px) { ... }
  • between-devices: limits the media query in a range using the first parameter as min-width and the second one as the max-width (substracting 1px from the max-width as well).

As you can see this approach has many advantages:

  1. The code is simple and easy to read: You instantly understand what is happening in the code.
  2. The current scope is clear: It's easy to indentify the code for a given device or screen size.
  3. It's very flexible: If you don't like the devices approach you could use screen sizes or your own style.
  4. Scales well: The implementation is encapsulated in a single file, if you need to make a change on a given breakpoint or add a new one its just a matter of modifying its value or adding the new one to the map structure.

Ok my dear developers I think that's it for now, I hope I was able to help you understand a bit more about media queries and breakpoints on CSS and SASS, sharing this approach with you is very exciting for me, if you have any comments about it you can send them over to duranenmanuel@gmail.com or on twitter @duranenmanuel.

See you in the next one.

Want to leave a comment? Do it on twitter

Found something to fix in the article? Edit it and send a Pull Request on GitHub!

Want to be the first to receive our cool updates?

© Copyright EnmaScript 2021-2022. All code based on the MIT license.