Just use Sass (SCSS specifically).
The actual most important rule is to co-locate styles for a selector, which is easier with Sass than without. The "traditional" method of placing all styles for a particular breakpoint in a single @media query at the bottom of the file (or eve worse, a different file entirely) is a good way to end up with spaghetti CSS that is difficult to override and impossible to maintain without introducing regressions.
Consider an everyday task for a CSS developer: "Go make the logo bigger/smaller on X screen size". How do you figure out where to put the new/changed styles? I suspect you might inspect the element in your browser to see how it displays at certain screen sizes, and then you go look for the selector (likely a class name) in the codebase. What happens if there are 12 results for .logo? Now you have to go scrolling around in the code to figure out which ones are nested inside of media queries in order to find the right place. Or maybe you just give up and add a 13th selector to the pile, making it worse for the next person (often you again).
In the examples below, the breakpoints in the existing file were collected into these variables:
$bp-xs: 480px;
$bp-sm: 576px;
$bp-md: 980px;
$bp-lg: 1150px;This is important because in the steps below you will be repeating media queries (say "no" to monolithic media query blocks!) and we don't want to introduce a new opportunity for human error by changing one query without changing the rest at the same breakpoint.
All media queries were also translated into range syntax. If I catch you using min-width and max-width I'm revoking your CSS Club membership. This makes it easier to reason about breakpoint direction and avoids style gaps between min- and max-widths.
Co-locate all properties and media queries for a particular selector. When you're done you will probably only have a single instance of the top-level selector in the file (in this case, .logo). It will become immediately obvious how many overlapping styles you have with slightly different selectors of various specificities.
Before the example below, these styles were spread out through out a larger file that was organized by breakpoint sections with a single media query encompassing all styles for that screen size. This was common in the early days of media queries, particularly in vanilla CSS in order to avoid repeated media query blocks.
.logo {
> a > img,
> a > picture > img {
vertical-align: middle !important;
max-width: 215px;
}
img {
max-width: 215px;
}
@media (width <= $bp-xs) {
text-align: center;
max-width: unset;
> a > img,
> a > picture > img {
max-height: 28px;
}
}
@media (width <= $bp-md) {
order: 1;
flex: 1 1 auto;
margin: 15px auto;
max-width: 215px;
> a > img,
> a > picture > img {
max-height: 40px;
width: auto;
height: 100%;
max-width: unset;
object-fit: unset;
}
}
@media (width <= $bp-lg) {
> a > img,
> a > picture > img {
max-width: 175px;
height: auto;
object-fit: contain;
}
}
@media (width > $bp-md) {
flex: 0 0 215px !important;
max-width: 215px !important;
min-width: 150px !important;
img {
max-width: 100% !important;
height: auto !important;
}
}
}Fix nested selectors by doing the same thing you did in step 1, and repeat until each child selector stands alone. Repeated selectors are prone to spaghettification. Ensure that media queries and other conditional blocks are moved to the top within a selector before nested child selectors are listed. Order media queries as best you can from smallest to largest, and nested selectors from least to most specific.
.logo {
@media (width <= $bp-xs) {
text-align: center;
max-width: unset;
}
@media (width <= $bp-md) {
order: 1;
flex: 1 1 auto;
margin: 15px auto;
max-width: 215px;
}
@media (width > $bp-md) {
flex: 0 0 215px !important;
max-width: 215px !important;
min-width: 150px !important;
}
img {
max-width: 215px;
@media (width > $bp-md) {
max-width: 100% !important;
height: auto !important;
}
}
> a > img,
> a > picture > img {
vertical-align: middle !important;
max-width: 215px;
@media (width <= $bp-xs) {
max-height: 28px;
}
@media (width <= $bp-md) {
max-height: 40px;
width: auto;
height: 100%;
max-width: unset;
object-fit: unset;
}
@media (width <= $bp-lg) {
max-width: 175px;
height: auto;
object-fit: contain;
}
}
}This is the first step where we'll actually be altering styles, so test your changes.
After reorganizing, you're likely to find that some of your media queries are totally unnecessary, especially if you use min-width or width > breakpoints. "Mobile-first responsive design" has been around since 2010, and even if you don't subscribe to the idea as dogma, you might realize that your first max-width is probably completely unnecessary because those are just the base styles and you can reduce the amount of conditionals in your CSS, thereby making it easier to understand. However, if you use max-width or width <= queries like our example above, you've probably painted yourself into a corner because it's more difficult to tell what the base styles for the element are since everything is conditional. And if there are a mix of min- and max-width queries, this might take a little while to fix, and you should think about what you did while you fix it.
Generally, you should be able to find the first max-width or width < query and assume those are the base styles, but any additional max-width queries will override those. That means ideally you'll convert everything into min-width or width > queries so that it's clear to everyone what the base styles are (i.e. if the screen size is 1) and how they are affected at larger screen sizes.
In our example we've got 3 breakpoints for .logo: <= $bp-xs, <= $bp-md, and > $bp-md. If we make the xs breakpoint the base styles we still have to contend with the md styles which are also being applied to the base. So now we combine all of the properties and see what overlaps. In this case there is actually a max-width being set and then unset at the smaller size, which translates into just setting the max width at the larger breakpoint and you get rid of the troublesome unset.
Sometimes, mixing > and <= may be acceptable in cases where otherwise you'd need to use an unset, which should be avoided. In this case, we only want to set order: 1 on the logo for the mobile header layout, so we use width <= $bp-md. In this rogue "max-width" query, we only set the least amount of properties necessary to make this work, and also leave a comment to explain why it's necessary.
.logo {
flex: 1 1 auto;
margin: 15px auto;
// Logo centered on mobile.
@media (width <= $bp-md) {
text-align: center;
order: 1;
}
@media (width > $bp-xs) {
max-width: 215px;
}
@media (width > $bp-md) {
flex: 0 0 215px !important;
max-width: 215px !important;
min-width: 150px !important;
}
img {
max-width: 215px;
@media (width > $bp-md) {
max-width: 100% !important;
height: auto !important;
}
}
> a > img,
> a > picture > img {
vertical-align: middle !important;
max-width: 175px;
max-height: 28px;
width: auto;
height: 100%;
@media (width > $bp-xs) {
max-height: 40px;
}
@media (width > $bp-md) {
max-width: 215px;
height: auto;
object-fit: contain;
}
}
}Our goal is to remove as many redundancies as possible, and use the least specific selectors that get the job done. First, remove all !importants. Don't ever use them, forget that !important exists. They should be the absolute last resort for when you're working within some terrible framework that you should have got rid of 2 years ago. If this "important" style applies at the base size, just don't override it; if it applies at a specific breakpoint or in some other specific context, great, you probably don't need the !important because of how CSS works.
In our example we have different selectors targeting the same img element inside .logo. We want to reduce specificity, so the more specific selector has to go. When we combine the styles, there are a lot of overlapping and contradictory styles, probably because someone was trying to override the base styles. Combine everything into the least-specific selector and test your changes. We know after reducing media queries what the base styles for img should be, and we can see that we actually want it to have a base max-width of 175px, not 215px, for instance.
.logo {
flex: 1 1 auto;
margin: 15px auto;
// Logo centered on mobile.
@media (width <= $bp-md) {
text-align: center;
order: 1;
}
@media (width > $bp-xs) {
max-width: 215px;
}
@media (width > $bp-md) {
flex: 0 0 215px;
max-width: 215px;
min-width: 150px;
}
img {
vertical-align: middle;
max-width: 175px;
max-height: 28px;
width: auto;
height: 100%;
@media (width > $bp-xs) {
max-height: 40px;
}
@media (width > $bp-md) {
max-width: 215px;
height: auto;
object-fit: contain;
}
}
}This is now significantly simplified from our original example, but it's still a bit difficult to reason about. We've got max-width and -height values all over the place for both the img and the parent .logo. Even just looking at .logo styles, we have both flex and min/max-width properties, and even I don't know off-hand which would win in a fight.
So, how to we reduce the "style specificity"? I recommend taking a top-down approach to styling with intrinsic design. This is not just another set of rules to memorize, it boils down to "work with the browser instead of against it". This serves us well because the result of using this approach is less explicit styles to maintain.
In our example, we have a .logo parent element and a child img, potentially with an a tag in-between. The entire purpose behind all of these styles is to make the logo a certain size at certain screen widths. Our goal again is to use the least amount of styles possible to achieve our goals, so the first thing we'll do is remove almost all of the styles on img aside from setting it to inherit it's size from the parent and object-fit: contain. Then, we can focus on just styling the .logo.
I've also added a display: block on the a & img to fix some spacing issues.
.logo {
flex: 1 1 auto;
margin: 15px auto;
max-width: 215px;
min-width: 150px;
// Logo centered on mobile.
@media (width <= $bp-md) {
text-align: center;
order: 1;
}
@media (width > $bp-lg) {
flex: 0 0 215px;
}
a, img {
display: block;
}
img {
width: auto;
height: 100%;
object-fit: contain;
}
}And there we have it, from 49 lines of spaghetti code spread out around a file down to 21 lines (that's a 57% decrease, or about 43% of the original size) all kept together, so when you need to make the logo larger on desktop you can get right to the .logo class selector and change one or two sizes all within the space of your editor window.
There are other things we could do, like putting that max size into a Sass variable, but that would add another line of code just for 2 instances of the value, so it's a wash. We could also remove the margin on .logo because the parent element should be handling child element spacing via gap and alignment via align-items, but that's another lesson.