What is the Element First approach and why use it?
We’ve tried both the “desktop first” and “mobile first” approach and have found that embracing either method 100% of the time can lead to equally bad practices, and messy compiled CSS. Over the years, we’ve opted to take an “element first” approach*, where we evaluate each element on it’s own and within the context of the layout and find the most efficient way write a rule on how it should respond at various breakpoints (there’s some more thoughts down below if you’re interested in learning more about this approach). Most SCSS mixins out there seem to only provide one or the other, so let’s build a mixin that not only give us the flexibility of deciding whether the rule is “min-width” or “max-width”, but also let’s us pass manual breakpoints for those instances where the rules you need to write don’t fit within the predefined resolutions you defined.
*Not to be confused with Element Queries, which is a javascript based solution used for writing queries based off the element, rather than the viewport. It’s an awesome project, but we’re not looking to involve javascript in this.
1) Let’s define which resolutions we want to support.
Below are the resolutions that we commonly use. To prevent certain resolutions from falling through the cracks, we serve the Phone experience at anything less than a standard size tablet resolution (768px) and the desktop experience at anything larger than a tablet in landscape mode (1024px). Let’s put them in a SCSS Map so we can easily loop through these in our media query:
// A map of breakpoints.
$breakpoints: (
phone-sm: 420px,
phone: 767px,
tablet: 768px,
tablet-lg: 1024px,
desktop: 1025px,
desktop-lg: 1600px
);
2) Let’s write the basic mixin:
@mixin for-size($breakpoint) {
// If the breakpoint exists in the map.
@if map-has-key($breakpoints, $breakpoint) {
// Get the breakpoint value.
$breakpoint-value: map-get($breakpoints, $breakpoint);
//Build the media query
@media (max-width: $breakpoint-value) {
@content;
}
}
}
This is a basic but functional start, and it loops through all the predefined resolutions we have defined. Usage would be as follows:
.my-element {
display:block;
@include for-size(phone) {
display:none;
}
}
One thing that would be nice is to be able to determine whether we want to make this rule follow mobile first (min-width) or desktop first (max-width). We’ll add this logic into the mixin:
@mixin for-size($breakpoint, $direction:down) {
// If the breakpoint exists in the map.
@if map-has-key($breakpoints, $breakpoint) {
// Get the breakpoint value.
$breakpoint-value: map-get($breakpoints, $breakpoint);
// Determine the direction and then write the media query.
@if $direction == up {
@media (min-width: $breakpoint-value) {
@content;
}
}
@else if $direction == down {
@media (max-width: $breakpoint-value) {
@content;
}
}
}
}
You might notice that we made the default direction of the mixin to be “down” or “max-width” or “desktop first” (line 1). This is personal preference and you could easily change the direction argument to “up”, which would mean you would add “down” to the inline mixin. Again, this is just personal preference. You could go a step further and have the default argument set to “null” and force you to pass a direction and throw an error if you don’t, but we didn’t feel it was necessary or helpful in our daily work.
If you’re familiar with SCSS logic, this should be pretty easy to understand. We introduced an argument of “direction” and allowed it to determine which type of media query this should be in terms of min or max width. In action: if we wanted to make sure an element is flexed on resolutions that can accommodate it (leaving the default behavior of “block” for mobile), usage would be as follows:
.my-element {
@include for-size(tablet,up) {
display:flex;
}
}
3) Let’s support wildcard breakpoints
We’re almost there, but one thing that we’re still missing is what happens when you need to write a media query that doesn’t fit within the predefined scope of resolutions? In a perfect world, every rule you write would fit within that scope, but we’ve had situations crop up where we’ve needed to modify an element at a resolution that doesn’t fit within the predefined breakpoints. It’s not super common, but it’s mighty handy to have available. So let’s make sure our mixin can accommodate manual resolutions being passed by adding an else statement:
// If the breakpoint doesn't exist in the map, pass a manual breakpoint
@else {
@if $direction == up {
@media (min-width: $breakpoint) {
@content;
}
}
@else if $direction == down {
@media (max-width: $breakpoint) {
@content;
}
}
}
This will allow us to pass any resolution we want right on the fly. We’ve also made sure to build in the direction argument:
.my-element {
@include for-size(1365px,up) {
padding-right:0;
}
}
There we have it. Here’s the full mixin:
/* Element First Media Queries
========================================================= */
// A map of breakpoints.
$breakpoints: (
phone-sm: 420px,
phone: 767px,
tablet: 768px,
tablet-lg: 1024px,
desktop: 1025px,
desktop-lg: 1600px
);
@mixin for-size($breakpoint, $direction:down) {
// If the breakpoint exists in the map.
@if map-has-key($breakpoints, $breakpoint) {
// Get the breakpoint value.
$breakpoint-value: map-get($breakpoints, $breakpoint);
// Determine the direction and then write the media query.
@if $direction == up {
@media (min-width: $breakpoint-value) {
@content;
}
}
@else if $direction == down {
@media (max-width: $breakpoint-value) {
@content;
}
}
}
// If the breakpoint doesn't exist in the map, pass a manual breakpoint
@else {
@if $direction == up {
@media (min-width: $breakpoint) {
@content;
}
}
@else if $direction == down {
@media (max-width: $breakpoint) {
@content;
}
}
}
}
Some thoughts on the Element First approach…
The overarching idea of element first is to avoid constantly “undoing” what you wrote, ie.. un-floating an element, un-inlining, un-positioning, un-resizing, etc.. which is arguably one of the major culprits of bloated stylesheets and conflicting styles. The key to integrating this approach into your workflow is to ask yourself when you go to write a rule: “Will the default behavior of this element suffice?” If not, then the second question would be “Which breakpoints do we need to modify to achieve the layout goal?”
For example, if you have an Unordered List that is meant to be stacked on desktop, but the mobile layout has the List Items inline to save vertical space, then applying a max-width rule (such as display:flex or display:inline-block) for only mobile resolutions would be called for, leaving the default stacked behavior for all desktop resolutions.
Inversely, if you had to position an element absolutely in relation to a parent element, but which would only function well on larger resolutions, then applying a min-width rule for desktop resolutions would be appropriate, leaving the default static position of the element for all smaller resolutions.
We’ve found using the desktop first or mobile first approach in both these scenarios required us to undo and re-write our rules depending on the element and resolution, and while it was easy to think in “one direction only”, it led to unnecessarily messy and bloated compiled CSS. I won’t avoid saying that element first is a bit more time consuming and really forces you to make what feels like an inordinate amount of decisions as you code, but the end result is lean and efficient stylesheets that leverage the cascade and the default behaviors of elements that are ideal for their context and resolutions. And after you put your mind in that state over and over, it becomes second nature and just as speedy as the other methods.
Responsive design can be really hard, but having these tools at your disposal can make it not only a lot less intimidating, but can also help you keep our compiled CSS file size down and make it easier to extend and maintain in the long run. We hope this encourages you to look at your own workflow and inspires you to adapt this approach for your own use.
Bonus snippet: Mixin for Between Sizes
The above is a great solution when you need a queries that apply to only one direction. But what if you need a bi-directional rule that would apply between a range of breakpoints? Using what we’ve already created, we can put this together pretty quickly. We’ll leave it up to you to dissect it and figure out the usage, but it’s commented for easy comprehension:
@mixin between-sizes($lower, $upper) {
// If both the lower and upper breakpoints exist in the map.
@if map-has-key($breakpoints, $lower) and map-has-key($breakpoints, $upper) {
// Get the lower and upper breakpoints.
$lower-breakpoint: map-get($breakpoints, $lower);
$upper-breakpoint: map-get($breakpoints, $upper);
// Write the media query.
@media (min-width: $lower-breakpoint) and (max-width: ($upper-breakpoint)) {
@content;
}
}
// If the breakpoints don't exist, allow manual breakpoints
@else {
@media (min-width: $lower) and (max-width: ($upper)) {
@content;
}
}
}