Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

CSS

Liam Maclachlan
Liam Maclachlan
22,805 Points

Sharing a maintainable and scalable media query mixin generator for SASS

Hey guys. This is off the back of a project I was working on for a robust media query based grid system, using a single simple map, key/value set-up. (you can see my Codepen for this project here)

After doing this, I realised that you will need to add custom media queries for when the content falls just outside the pre-set screen widths.

This was my solutions. (Codepen here) Code below:

////////////////////////////////////////////
// function for finding next value in map //
////////////////////////////////////////////
@function nextKey($current-value, $mapped-list:$sizes__map) {
    // get current key from list
    $the-list: map-keys($mapped-list);

    //find index of current value and add 1
    $the-index: ( index( $the-list, $current-value) ) + 1;

    // get value from list with new index
    $new-value : nth($the-list, $the-index);

    @return $new-value;

}

///////////////////////
// Media breakpoints //
///////////////////////
$sizes__map : (
    small: 1px,
    medium: 768px,
    large: 960px,
    largest: 1080px,
    xlargest: 1240px
);

///////////////
// The mixin //
///////////////
@mixin media($breakpoint, $breakpoint__two:null) {
    // setting up variables for the second query, if needed
    $first-query: map-get($sizes__map, $breakpoint);
    $second-query: map-get($sizes__map, $breakpoint__two);

    // checks for single or dual arguement inputted
    @if $breakpoint__two == null {
        $mapped__keys : map-keys($sizes__map);

        // get current index of breakpoint to decide what query to run
        $index : index($mapped__keys, $breakpoint);
        @if $index {
            // if it is not the last item in the map, run this
            @if $index < length($sizes__map) {
                @media screen and (min-width : map-get($sizes__map, $breakpoint ) ) and (max-width: map-get($sizes__map, nextKey($breakpoint) )  - 1 ) {
                    @content;
                }
            }
            // if it is the last in the map, run this
            @elseif $index == length($sizes__map) {
                @media screen and (min-width : map-get($sizes__map, $breakpoint) ) {
                    @content;
                }
            }
        }
        // if key does not exsist, run this
        @else {
            @error "Sorry but #{$breakpoint} is not a breakpoint in your setup";
        }
    }
    // will call if two arguements inputted are the same type
    @elseif type-of($breakpoint) == type-of($breakpoint__two)  {
        body {
            color: purple!important;
        }
        // if breakpoint is a string
        @if type-of($breakpoint) == string {

            // if both values are in the $sizes__map
            @if $first-query and $second-query {

                // corrects ordering of variables for correct media query output
                @if $first-query < $second-query {
                    @media screen and (min-width: $first-query) and (max-width: $second-query - 1) {
                        @content;
                    }
                }
                @else {
                    @media screen and (min-width: $second-query) and (max-width: $first-query - 1) {
                        @content;
                    }
                }
            }
            @else {
                $mapped__keys : map-keys($sizes__map);
                @error "The given arguments are not valid you must choose from: #{$mapped__keys}";
            } 
        }
        @elseif type-of($breakpoint) == number {
            // corrects ordering of variables for correct media query output
            @if $breakpoint < $breakpoint__two {
                @media screen and (min-width: $breakpoint) and (max-width: $breakpoint__two) {
                    @content;
                }
            }
            @else {
                @media screen and (min-width: $breakpoint__two) and (max-width: $breakpoint) {
                    @content;
                }
            }
        }
        @else {
            $the-type: type-of($breakpoint);
            @error "passed invalid arguement type #{$the-type}";
        }
    }
    // if neither breakpoint is of the same type
    @else {
        @error "Inputted values are invlaid. Must be array variables or matching unit types";
    }
}
////////////////////////////
// Ways to call the mixin //
////////////////////////////

@include media(small) {
   body {
      background: blue;
   }
}

@include media(small, large) {
   section {
      background: orange;
   }
}

@include media(640px, 1080px) {
   footer {
      background: green;
   }
}

@include media(30em, 60em) {
   header {
      background: red;
   }
}

And outputs:

@media screen and (min-width: 1px) and (max-width: 767px) {
  body {
    background: blue; } }
body {
  color: purple !important; }

@media screen and (min-width: 1px) and (max-width: 959px) {
  section {
    background: orange; } }
body {
  color: purple !important; }

@media screen and (min-width: 640px) and (max-width: 1080px) {
  footer {
    background: green; } }
body {
  color: purple !important; }

@media screen and (min-width: 30em) and (max-width: 60em) {
  header {
    background: red; } }

It does still require the units to be the same size but I don't want to change this as I think it will cause more harm than good to the robustness of this mixin

Let me know what you think, guys. Any improvement etc. If you want to use this code, feel free :)

1 Answer

Tim Knight
Tim Knight
28,888 Points

Nice work Liam, this is similar in goal to a mixin I blogged about last year that you might be interested in at https://medium.com/developing-with-sass/creating-a-dead-simple-sass-mixin-to-handle-responsive-breakpoints-889927b37740

I later refactored it to remove the type checking and just use maps which could hold either one or two values which can be seen at https://medium.com/developing-with-sass/refactoring-my-simple-sass-breakpoint-mixin-71205dd6badd. It just became easier that way so I could keep all of my configuration in a single place instead of providing the flexibility of adding media queries randomly in the code base.

I like where you went with comparing sizes to determine min and max. I think you could do some really cool things with that, for example changing the attribute to check for a list and using "to" as your delimiter:

@include media(400px to 800px) { ... }

Then if it's a list of more than one item with "to" as the delimiter you could parse it out the same way. Just thought it would be a fun idea to play with. Nice work.

Liam Maclachlan
Liam Maclachlan
22,805 Points

Hey man. I like the post (I'm a bit under the weather today so sorry if I got the wrong end of the post). I really wanted to make the Mixin as maintainable as possible from a single map and flexible enough to be able to add custom queries on the fly.

As you pointed out with Bootstrap, it works within set queries. Personally, I find that too limiting and doesn't take in to consideration the variations in the content and the potential site design, that may need custom queries.

I really like the reiteration of the project. It's probably worth mentioning that this came around because of the project that preceded this one, for a flexible grid system generator here, and I wanted it to be as 'smart' as possible.

Thanks for the suggestion on the 'to' approach. I'll definitely look in to it :)

Liam Maclachlan
Liam Maclachlan
22,805 Points

Okay. I've merged what i saw in your posts and built it around my map config and came out with this:

@mixin breakpoint($min: null, $max: null) {
    $query: '';
    @if $min {
    $query: append($query, '(min-width: #{map-get($sizes-map, $min)})')
    }
    @if $min and $max { 
    $query: append($query, 'and')
    }
    @if $max { 
    $query: append($query, '(max-width: #{map-get($sizes-map, $max)})') 
    }
    @media screen and #{$query} { @content; }
}

Not too dissimilar but works well for working off the map framework. And also solves to problem I didn't realise I was having (untill I started using it) about being able to set a 'max-width' only value :)

This provides enough flexibility whilst also making sure that a structure is followed

Tim Knight
Tim Knight
28,888 Points

Nice job Liam, I'm glad the code provided some inspiration.