Better media queries
Anyone who has read through CSS, whether their own or someone else's, will know that sometimes media queries can be messy.
Who hasn't inspected an element in dev tools and seen dozens of style rules being applied, only to be crossed out because they are overridden by a media query? Or scrolled through a CSS file, struggling to piece together a clear picture of how a component is styled from having rules scattered throughout a dozen media queries?
This lack of clarity results when one of the two most common ways of approaching media queries are taken: 'mobile first' and what I would describe as 'targeted'. Both have their advantages, but both have distinct issues too, so in this article I will discuss these and outline instead an alternative approach that solves these issues.
Mobile First
The principle behind this approach is that the styles outside of media queries are intended for mobile. If styles for a larger screen size differ then they are placed in a min-width media query. If styles for an even larger screen size differ, they are added to the next min-width media query, and so on.
Here's an example:
.navigation {
display:flex;
flex-direction: column;
gap: 8px;
font-size: 1rem;
padding: 16px;
border: 1px solid black;
}
@media all and (min-width: 601px) {
.navigation {
flex-direction: row;
gap: 16px:
font-size: 2rem;
padding: 0;
border: none;
}
}
@media all and (min-width: 1201px) {
.navigation {
font-size: 2.5rem;
color:red;
}
}
The benefit of this approach is that mobile devices get all the styles they need, and can ignore the media queries entirely, which is faster, and speed is more important on mobile devices than it is on larger devices. It also makes the styling for mobile devices very clear.
The problem with this approach is that larger screens are being told to apply the same rule multiple times. In the above example a screen 1201px wide is being told to apply a font size of 1rem, then 2rem, and finally 2.5rem. Looking at the result of this CSS in dev tools and you'll see a whole lot of crossing out (never a good sign). This is inefficient and impairs code legibility.
Additionally, it is hard to visualise the differences between the screen sizes from looking at the code - for example, looking at the min 601 query in isolation, there is no indication that there are larger screen sizes where the font size is even larger.
In short - this approach is good for mobile, but less so for larger screens, and isn't very easy to interpret.
Targeted approach
So how about the more targeted approach? In this approach every screen size is considered equal. The only styles that exist outside of media queries are those that apply to all screen sizes. Mobile specific styles are inside media queries, as are those for larger sizes, and max-widths are utilised in those queries.
Let's take a look:
.navigation {
display:flex;
}
@media all and (width <= 600px) {
.navigation {
flex-direction: column;
gap: 8px;
font-size: 1rem;
padding: 16px;
border: 1px solid black;
}
}
@media all and (width > 600px) {
.navigation {
flex-direction: row;
gap: 16px:
}
}
@media all and (width > 600px) and (width <= 1200px) {
.navigation {
font-size: 2rem;
}
}
@media all and (width > 1200px) {
.navigation {
font-size: 2.5rem;
color:red;
}
}
Looking at this code in dev tools on a larger screen size and it is immediately apparent that everything is precisely targeted because nothing is crossed out and overwritten. That's much better.
In many respects the code is also easier to read. Looking at the 600px to 1200px query it is clear that a larger font size must be applied at sizes above that range without having to see the media query below it - unlike the 'mobile first' approach where the min 601px media query in isolation tells us nothing about what the font size might be like on larger screens.
But this approach has problems of its own. Most obviously, mobile devices now have to read media queries to get their styling. This probably isn't a huge deal from a performance perspective but it isn't ideal.
It is also still quite hard to gain an overall picture of the style rules being applied, and whether those rules differ between screen sizes, or whether just the values of those rules differ. To take the width <= 600px query, for example, we don't know whether the 'padding: 16px' is unique to that screen size because the value of 16px is unique, or whether the application of padding of any size is unique.
An alternative approach
What if we could write media queries that provide the benefits of both approaches while avoiding their drawbacks? Well, by utilising CSS custom properties we can.
This approach involves:
- placing all style rules specific to a screen size inside a media query
- where a rule is shared between all screen sizes but the values differ, assigning the values to custom properties within media queries for all screen sizes except mobile
- placing rules common to all screen sizes outside of media queries, using the custom properties defined in media queries where the values differ, with the mobile value as the fallback
It's easier to illustrate with an example:
@media all and (width <= 600px) {
.navigation {
border: 1px solid black;
padding: 16px;
}
}
@media all and (width > 600px) {
.navigation {
--navDirection: row;
--navGap: 16px:
}
}
@media all and (width > 601px) and (width <= 1200px) {
.navigation {
--navFont-size: 2rem;
}
}
@media all and (width > 1200px) {
.navigation {
--navFont-size: 2.5rem;
color: red;
}
}
.navigation {
display: flex;
flex-direction: var(--navDirection, column);
gap: var(--navGap, 8px);
font-size: var(--navFontSize, 1rem);
}
Let's break this down.
The first media query has properties unique to mobile (border and padding). The other media queries assign values to custom properties for the properties that all screen sizes share. We additionally apply the color rule specifically for screens above 1201px.
Finally we write all the rules common to all screen sizes. Where values differ between screen sizes, we use the custom properties, with the fallback being the mobile value (eg. on a mobile size screen --navFontSize will not be defined, so will render 1rem).
This approach has clear advantages. Firstly, in many cases mobile devices will not need to read media queries at all, as most of the styles will be applied via fallbacks, so it retains much of the benefit of the mobile first approach. At the same time, unlike the 'mobile first' approach, larger screens do not have override rules set by mobile, therefore sharing the benefit of the 'targeted' approach.
Additionally this approach is the clearest to read, as the intention of each line of CSS is explicit. It is very clear for example, when reading the min-width: 1201px media query, that the entire color rule (not just the value of red) is specific to that screen size, whereas the value of 2.5rem is specific to that screen size but the setting of the font size property is not. This makes it much easier to decipher how media queries are interacting with one another.
Additionally a quick scan of the .navigation selector outside of the media queries shows the majority of CSS rules that are applied to the selector, and a clear idea of which of those rules have variable values.
I have been using this approach for a few years, and have found my media queries a lot easier to manage, particularly when returning to the code months after it was originally written, or when unpicking the styles in dev tools. If you haven't tried this approach before then give it a try!