Using Sass Breakpoints Effectively
There have been plenty of blog posts touting the reasons to use Sass as a CSS preprocessor, and if you've been doing responsive design for a while, you're probably already using the Breakpoint gem with Sass. But there are many ways to use both of these tools, so let's talk about using breakpoints effectively.
Start with the small screen first, then expand until it looks like sh*t. Time for a breakpoint!
- Stephen Hay.
I must preface by saying that using device-based media queries *only* is not necessarily the best approach. It often makes more sense to inject breakpoints when you need them for a particular component. Some examples of when to add a breakpoint include:
- Based on your font size & the size of the parent container, the line length may extend beyond the recommended number of characters for optimum readability (50-60 characters). Time for a breakpoint!
- Based on size of images within that container and text wrapping around those images, your layout no longer "works" visually, or information becomes disjointed or confusing to understand. Time for a breakpoint!
- It looks funky. Time for a breakdance! I mean breakpoint!
That being said, it is still common that during the design process, teams will break down the design into stages for smartphones, tablets, and desktop. So if you are being asked to implement designs like these, it is likely that you'll end up defining device-based breakpoints, even if you may deviate from them occasionally for reasons listed above.
For me that meant that I would begin with variables like the ones below, which is a good start.
$phone-p: 319px $phone-l: 479px $tab-p: 639px $tab-l: 769px $desk: 1020px
(Since we're talking about Sass and breakpoints, you can follow along and see a demo on Sassmeister.com.)
Min and Max Queries
Now for a while, I was using these variables for both min-width and max-width queries, which wasn't noticeable at first since the majority of queries tend to be min-width, because I use a mobile-first approach, meaning I begin at the mobile size and add on styles as the viewport gets larger. But there are times and places where it is more DRY to use a max-width query, especially if you are styling a component that only appears on mobile, like a mobile menu. Here's where I noticed the problem.
If you use the same values for both min-width and max-width queries, you'll end up with overlapping styles at the 1px where those queries overlap. You'd either see whichever styles were more specific or printed last in the CSS, or more likely a combination of both. To avoid this, if one media query stops at 770px, the next should start at 771px. For those of you playing along at home, if you have the Sassmeister example open and you squish the window to tablet size, you'll see that the box is either blue or has a pink border, but never both—which is what we want! Think of the pink box as the "mobile" menu and the blue box as the "desktop" menu.
So my first set of breakpoints are just for use with max-width, the second set are for use with the default regular breakpoint mixin, or max-width queries.
// For use with max-width only: // example: +breakpoint(max-width: $tab-l) $phone-p: 319px $phone-l: 479px $tab-p: 639px $tab-l: 769px $desk: 1020px $desk-full: 1079px // For use with min-width (default breakpoint mixin) // example: +breakpoint($desktop) $smartphone-portrait: 'screen' ($phone-p + 1px) $smartphone-landscape: 'screen' ($phone-l + 1px) $tablet-portrait: 'screen' ($tab-p + 1px) $tablet-landscape: 'screen' ($tab-l + 1px) $desktop: ($desk + 1px), 'no-query' '.lt-ie9'
This might seem like a a fringe case scenario, but it's a problem we can easily avoid.
Another advantage of this system: notice that the $desktop variable has the no-query fallback built in, which makes compiling a separate stylesheet for IE8 much easier; more on that in a minute. But If i were to try to use that the $desktop variable with max-width:
// don't do this: $desktop: ($desk + 1px) ,'no-query'; '.lt-ie9' +breakpoint(max-width $desktop)
it would throw an error! So its preferable to have two sets of variables, one for min-width and one for max-width.
And finally, I'd definitely encourage you to look into using a separate fallback file to support older browsers. What this means is you end up with one ordinary CSS file with all your media queries, as you intended, and then you have a second CSS file with *no* media queries, but instead contains a prefix class like .lt-ie9 (less-than Internet Explorer version 9) for all queries meant for desktop browsers.
To implement this solution, set up your Sass files like this:
// Import Base theme base variables, mixins and extends: @import "base_utilities" // Then import base styles @import "base_components"
$breakpoint-no-queries: true $breakpoint-no-query-fallbacks:'.lt-ie9' !global // Re-import everything from styles.sass but without media queries. @import "styles"
Then enable the Conditional Styles module and add this to your theme.info file:
stylesheets[all] = css/team-elements.css stylesheets-conditional[lt IE 9][all] = css/team-no-query.css stylesheets-conditional[(gte IE 9)|(gt IEMobile 7)|(!IE)][all] = css/team-styles.css
You can read all about the no-query fallback methods on github, but for what it's worth, setting up the desktop variable with the no-query fallback body class on it will ONLY print both the media query and the fallback if this flag is set:
$breakpoint-no-query-fallbacks: '.lt-ie9' !global
Your CSS will include the lt-ie9 styles only (no media queries) if this flag is set:
The following section is not for the faint of heart
You may have seen Chris Eppstein's blog post a few days ago titled, "Balancing Complexity in Sass", which was very on-point in many ways. Sometimes we create such abstract code that it may add more confusion than it prevents. This just might be an example of that, but I thought I'd share in the hopes that perhaps someone has a cleaner way of implementing something like this!
To save time, I wrote up some shorthand variables for all the combinations of the min and max breakpoints, which you can also view on Sassmeister. Maybe it's unecessary? Maybe there's a better way? Maybe I'm variables crazy? Perhaps! Anyway, here's my whole breakpoint partial for your enjoyment.
// BREAKPOINTS $breakpoint-to-ems: true $print-media: 'print' $hidpi: min-resolution 143dppx // For use with max-width only: // +breakpoint(max-width: $tab-l) $phone-p: 319px $phone-l: 479px $tab-p: 639px $tab-l: 769px $desk: 1020px $desk-full: 1079px // For use with min-width (default breakpoint mixin) // +breakpoint($desktop) $smartphone-portrait: 'screen' ($phone-p + 1px) $smartphone-landscape: 'screen' ($phone-l + 1px) $tablet-portrait: 'screen' ($tab-p + 1px) $tablet-landscape: 'screen' ($tab-l + 1px) $desktop: ($desk + 1px), 'no-query' '.lt-ie9' // use // $breakpoint-no-queries: true // $breakpoint-no-query-fallbacks: '.lt-ie9' !global // at the top of a panel layout sass file to // re-render everything with .lt-ie9 for desktop media queries. ////////////////////////////// // COMBINATIONS // Phones $max-phone-p: 'screen' (max-width $phone-p) $max-phone-l: 'screen' (max-width $phone-l) $phone-p-phone-l: 'screen' ($phone-p + 1px) $phone-l // Tablets $max-tab-p: 'screen' (max-width $tab-p) $max-tab-l: 'screen' (max-width $tab-l) $phone-l-tab-p: 'screen' ($phone-l + 1px) $tab-p $phone-l-tab-l: 'screen' ($phone-l + 1px) $tab-l $tab-p-tab-l: 'screen' ($tab-p + 1px) $tab-l $tab-p-desk: 'screen' ($tab-p + 1px) $desk $tab-l-desk: 'screen' ($tab-l + 1px) $desk // All Mobile $mobile-device: 'screen' max-width $desk // High Resolution Devices $mobile-hi-res: 'screen' (max-width $desk) ($hidpi) $desk-hi-res: 'screen' (min-width $desk) ($hidpi) //Bonus: Mixin for >IE9 *only* with no media queries @mixin fallback @if $breakpoint-no-queries == true .lt-ie9 & @content
Hopefully this has been helpful, and I'm most definitely welcoming thoughts, comments and suggestions!