Write CSS as CSS in CSS. It is the best way to learn, and it gives us the best code quality because it leans on language and platform.
Anything else tends to turn out not to be CSS at all, maybe fast to write at first but it makes us fall behind on the language and its evolution.
Strong beliefs, weakly held #
We are programming the browser, not painting with code or assembling LEGO.
In any kind of development, our experiences accumulate into repeatable practices that we adjust over time. Knowing and working with the languages last longer than the dogma we were so eager to follow five years ago.
When looking for the ultimate way to do something, we are often trying to avoid starting from scratch every time. And sometimes, people want shortcuts, usually in the form of other people’s code. With experience in writing code ourselves, we can stop looking and instead build upon what we have done before. By continuing to improve ourselves, we can also use new features from day one.
CSS should be written, structured and set up for the individual project. And with a base approach that we can adjust based on needs and new language features, it’s easier to get going. The following is how I currently structure CSS, along with a closer look at the effects of utility usage.
Setup for the future #
I use PostCSS and plugins to transpile and minify. Writing CSS that doesn’t yet have full browser support is unproblematic as long as we can use such tools or otherwise progressively enhance. When features become widely available, and compatibility isn’t an issue, we remove the corresponding plugin and ship even lighter code, like with variables when support for Internet Explorer was dropped.
Depending on what’s required, I currently use Webpack or Vite; the config is almost the same. Webpack is more flexible, but Vite is faster. The important thing is that the tool does the job reasonably efficiently, and we take the time to understand how to configure it.
Structure #
I have all CSS in one folder, in separate files based on type or what they cover. All files are imported in index.css; an example from one of my projects:
@import 'reset';
@import 'vars';
@import 'base';
@import 'components';
@import 'layout';
@import 'frontpage';
@import 'areas';
@import 'page';
@import 'utilities';
More reusable than components #
I don’t operate with a one-to-one relation between markup and CSS, the two work in different manners. It’s easier to see how the different CSS parts affect each other and the markup when everything isn’t modelled on the sitemap.
Even for components (which are just smaller templates), I find it inefficient to mirror that in the CSS. I would have to strengthen specificity and scope, repeating code for pieces made unavailable to the other parts. I try to reuse instead.
CSS is not only in use where we define it, which sounds like something we have to fight but, except for a few resets, it’s something we should lean into. The interface works even if we haven’t defined CSS. It means we don’t start at zero every time; there is already an order to the defaults which we can build from.
We can create a base style that works across all our interfaces, using mostly elements as selectors. It’s some of the most effective code we can write. By defining a button style in the base using the button element as selector, we only need modifier or context classes later to create variants.
CSS for CSS #
My projects always involve a trio of files that affect all pages without being referenced directly in the markup in the way classes are. With a good foundation, we can build on top instead of overriding – again, leading to fewer lines of code and more apparent relations between the different parts. I find it more fruitful to be able to see what’s happening by reading the code than looking at the interfaces.
Reset
I always write my own resets because they significantly affect many elements. It would defeat the purpose to add someone else’s code for that. If we don’t write our own from scratch, we can always base it on someone else’s, but we must make it our own. Know what it does, update it to our needs and remove what we don’t need.
And never, ever have the CSS reset as a dependency; it only leads to zombie CSS. That’s someone else’s code written for their project and needs, they can do whatever they want, and they will; changes have nothing to do with our code and project. We lose control over our code to them.
Variables
I use custom properties sparingly. Primarily for colours and font size variables. One or two shadow declarations, and for standardising transition and border-radius.
If I have dark mode, I also make a second level of variables here where the original set serves much bigger dark and light mode sets.
Base
Element selectors, like a, h, p and legend, get a base styling, size, colour, and weight. The variables are already put to good use here.
Specific CSS #
A collection of template and template-ish CSS will be the biggest parts. Although all parts of the CSS are open to changes over time – they’re not made linearly – these bigger pieces see the most work and changes. I like to split them into at least one for layout and one for individual pieces like templates and components.
Components
These are CSS components, reusable pieces of CSS that flexibly correspond to structured HTML. At this point, I start using CSS nesting to lightly scope and write less code without mirroring the HTML.
Layout
The layout file contains the major layout pieces of a project that I can reuse in many places. I also place global navigation here. Navigation that is not part of the majority of the pages, can be placed with the components instead.
Templates, sections and shared snippets
These are the more individual areas within a site or app, like the front page, sections, articles and documentation.
With a complete CSS concept that isn’t too tightly locked in scope, we can produce component variants where they are used. That can seem counterintuitive, but it’s about using the context instead of defining explicit variants. If we have a card component, we can make a variant by nesting its selector inside a relevant location-specific class and applying local settings. In contrast to a JS world where we define a component variant and pass a prop to it, we automatically define the variant by its placement. We can use it without applying classes that tell it what to be, and when we don’t need the variant anymore, chances are we have already deleted the code for it.
However, I might make a new component directly inside the template file along with related code if there are many overrides. I only move it to the components file if it is to be reused elsewhere, not just inside one page or location. The differences aren’t always crystal clear, and we need to be able to discuss what to do based on different cases – it’s a balancing act.
Things can get messy here if we dump leftover classes we don’t know where else to put. But if we have a good structure and order and adjust it when necessary, we are less likely to end up with many loose pieces.
I sometimes go against many of these principles and create a dedicated file for an area. For a front page that usually differs from all the other pages and doesn’t share as much code, I think it’s better to have most of it in one place than split into smaller pieces. It boils down to reusability versus scope. If something is clearly a component through code and scope, but is only used in one area, it stays in the dedicated area file and doesn’t make it to the components file.
Sensible utilities #
I used to reach for utilities because they seemed reusable, didn’t cause much conflict, and I could make stuff without having to think much. But after a couple of years, I realised that good code doesn’t come from using shortcuts and avoiding thinking; it comes from systemising. How we write and structure frontend, which is basically a design system, is more efficient, flexible, versatile and available than pre-made blocks of code that we assemble. Thinking is essential to programming; we cannot avoid it if we want to make quality solutions. It might even lead us to conclude that some utilities are still helpful.
Now, I use them for local adjustments, primarily for spacing. Otherwise, the occasional max width or text alignment for elements without any other specific styling.
Utilities last
By importing utilities last, they can override properties in other classes that don’t have a stronger specificity. I always write my own utilities and try to keep it to only what is needed. If we have no rules for where to draw the line, utilities increase bloat and technical debt while making us no better at writing CSS.
If an existing declaration exists, I modify that instead of applying a utility. The utilities are not the preferred way to achieve what they do; they are backups, a way to avoid writing dedicated classes for one or two properties. If an element has more than three or four utilities, I usually make a dedicated class instead. Too many utilities give messy HTML, and we are left with a major task if we decide to change something. Cleaning up utilities is not fun.
I avoid using utilities for layout work. Unless making only the most basic flexbox layout, I write flexbox, grid and company as dedicated classes.
I also avoid making utilities with breakpoints. I used them for a while but found many signs of lousy programming: Hundreds of lines of code lying around just in case. Complete sets of spacing utilities in four sizes and three different viewport sizes end up at somewhere between 400 and 500 lines of code. There are ways to remove unused CSS when bundling, but that will require more config and a rigid setup to remove something that shouldn’t have been written in the first place.
Using is not writing
I’m less strict on utility usage in work settings where many people share the same code. It helps us make interfaces a bit faster and more coherent. Utility libraries use the same argumentation. But applying a class to an element is not the same as writing CSS. I don’t think we become better developers or make better interfaces, because it becomes a reason not to work with CSS. We remain oblivious to its strengths and continue to work against the platform. We fall behind, and adopting new frameworks and libraries is not keeping up; it’s just the same stuff over again. There is also no doubt organisations use design systems to get by with less frontend competence, but that’s a different argument.
Productivity concerns may outweigh weakly held strong beliefs in the short run. Moving forward and expanding our knowledge takes time. That’s why I like to write and push articles, workshops and experiences with actual CSS. Best practices evolve and adjust, but frameworks and libraries perish.