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:
- It has hacky/ackward breakpoints and media queries.
- It's not clear what screen or devices each media query is covering.
- It generates inconsistencies and issues that are hard to fix and often require a hacky solution.
- 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 themax-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 themax-width
(substracting 1px from themax-width
as well).
As you can see this approach has many advantages:
- The code is simple and easy to read: You instantly understand what is happening in the code.
- The current scope is clear: It's easy to indentify the code for a given device or screen size.
- It's very flexible: If you don't like the devices approach you could use screen sizes or your own style.
- 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.
Originally posted at Enmascript.com
Top comments (1)
Thank you for sharing Enmanuel. 🙏
As I've learned about media queries and applied across projects I always had these questions. With device screens changing all the time how do we define breakpoints? Is there a pattern to define them?
This said, I'm glad I came across your post and will give this a try! Is there anyone else out there who created a resource about this topic?