A Modern CSS Methodology
- Published 2025-05-21
Modern CSS is a fantastic, powerful language blighted by a murky past. Lack of expressiveness in older versions led to common usage of messy workarounds and layers of preprocessors like Sass, both of which have remained common even though they are no longer needed. This is compounded by what I see as the one major remaining pain point of the language—it’s very easy to write messy code that grows organically into an unmaintainable mess. Some oversight and discipline is required to stop this happening.
What follows is a set of guidelines for things I have found work well to write CSS in a way that leads to readable and maintainable code. It’s also noteworthy that working this way is also likely to increase the accessibility of your site, making it easier for technology such as screen readers to correctly interpret the content.
Context & Specificity
What we want to avoid is “div soup”, or “divitis”, i.e. layers and layers of nested divs that create HTML (or insert your templating language of choice code) which is hard to read and maintain. We can do this in a few ways.
Semantic HTML
Modern HTML has a lot of tags available that you might not have heard of. How about:
Of course many of these you might never need to use, but there is often a “correct” HTML tag to use in a
situation where you might otherwise be tempted to simple use a <div>
. Using the HTML tag as designed
makes the intent of the code easier to understand, keeps your HTML and CSS cleaner and reduces the need for custom classes.
Attributes
Often, we’ll be using the same element for slightly different purposes, and need to distinguish between these
purposes for styling. Here, we can often rely on attributes. For example, the <input>
element has many
different types: text, email, password, checkbox etc. In our CSS we can utilise these to limit our styling to only the
relevant elements.
input[type=checkbox] { scale: 2 }
input[type=password]::after { content: url("padlock-icon.svg") }
But what if the attribute to distinguish our elements doesn’t exist? Make one up! Custom attributes are nothing to avoid. Continuing with the input example, there’s nothing to stop us doing something like this:
input[admin-only] { background-color: red; }
input[admin-only] { background-color: red; }
<input type="text" name="access-level" admin-only></input>
Scoping & Nesting
One thing that can easily become a struggle in CSS is ensuring that styling an element in one place doesn’t have unintended consequences elsewhere. It can often feel safer to simply introduce a new class with the styles you need, even if this is likely replicating existing code. Taking this approach might be easier in the short-term, but it’s simply not sustainable. The solution is to make sure we scope correctly, and style with the correct level of specificity.
Using attributes as mentioned above helps increase our specificity, so we limit our styles to the correct type of element. Pseudo-selectors can also help, which I’ll come on to later, but first let’s talk about scoping.
Modern CSS supports nesting, which was a long time coming and one of the primary things which drove people to Sass and its ilk. I would encourage you to always nest appropriately. For example, perhaps you’re adding a menu to your site that looks something like this:
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
</nav>
The CSS, then, should be structured something like this:
nav {
ul {
margin: 0;
li {
list-style-type: none;
a:link, a:visited {
text-decoration: none;
}
}
}
}
This immediately keeps all the changes we make scoped to this one place, and won’t affect other lists or links. It also makes the
intent of the code clearer, and allows future developers (this includes you) to be more confident when updating CSS: it’s clear at a
glance where this could affect. If, in the future, you add another <nav>
element which requires different styling,
it’s a one-line change to your CSS to increase the specificity of the selector: simply add an attribute to differentiate them, e.g.
<nav level="site">
, or perhaps nest this CSS inside header
. Use your judgement what makes
the most sense in a given situation.
I find nesting meets most of these needs, but you should also be able to reach for combinators when they’re more appropriate.
Custom elements
When the HTML spec doesn’t give you an element that makes sense, you can simply… make your own! It’s fine to just invent your
own tag name, so long as it includes a hyphen. For clarity, it could make sense to prefix them with the name of the app or
organisation, e.g. <myorg-calendar>
. Web
Components rely on this approach, so it works very well alongside them. I find this works best when you have what feels
like a custom, reusable object.
Use pseudo-classes
There are a wide number of these that allow you to select an element which is in a given state, or has a certain property, and these are another great way
to increase the specificity of your selectors. Past the well-known ones such as :hover
, there are other super useful options
such as :empty
, :local-link
, :nth-of-type
and :valid
, not to mention :has()
which the CSS community has been waiting on for years. It’s well worth browsing
what’s available.
Classes
Classes should be used primarily for styles which you may want to apply widely across a number of elements, and where the same
element is often used with or without that styling. For example, if you have some images which require a large margin, adding a
margin-large
class to these is fine. If all images, or at least all images of a certain type/scope require this
style, however, add the styling directly to the element and forget about the class.
Use modern CSS features
As I mentioned earlier, one issue with CSS is that for years complex styling required a number of workarounds and hacks. Many developers have never learned the newer ways of doing things, resulting in a dislike of the language and outdated poor code proliferating. If this is you, learn these newer methods. Make sure you understand Flexbox and Grid layouts. You don’t need to know everything, but you should have an idea of what is possible, and if you find yourself writing something hacky, check if there’s a better way—there likely is.
Variables
Any values used throughout your code should, as in other languages, be stored as variables. This means you’ll only have a single place to change the value if you need to, protects against values being different by mistake and, if named well, makes the code more readable.
Organise your code
In terms of file structure, I’ve settled on a fairly straightforward system.
- Site-wide styles
- Styles to be applied as element-defaults, site-wide (e.g. default link styling) go in a single file, appropriately named.
- Variables
- Keep widely used variables in a single file. Variables used only in one place can be kept elsewhere, as appropriate.
- Layout
- Depending on the complexity level this may or may not be necessary, but having a single file that puts all the building blocks in place for the primary layout of the site can be useful if it’s involved.
- Object-specific files
- For almost everything else, I find one file per-“object” works well. I think the notion of an object in this context is fairly intuitive but a little hard to describe. It is your top level of nesting, so a single object CSS file will have one selector at the top, with everything else nested inside. (Almost. It can also contain the same element with additional selectors appended at the top level, and media/container queries, but they should then only contain the single object.)
- Reset
- It’s debatable how necessary a CSS reset is in this day and age, but there are some benefits. If you’re using one, keep it separate and ensure it’s loaded first.
ARIA
TODO