When I first heard Nicole Sullivan talk about OOCSS, I thought “Oooh, smart.” When I read Jonathan Snook’s riff on that idea in SMACSS I thought “Oooh, smart.” When I heard Harry Roberts say “never use IDs in your CSS files” I said “Oooh, smart.”
But when BEM and roboclasses came around… I didn’t have the same reaction. I never felt attracted to these tools even though thousands of developers had success with them. I’m not sure why I never jumped in, but I’m sure verbosity and/or tooling fatigue played a part. Ultimately, I ended up settling on my own SMACSS/BEM hybrid that’s more of a .block--variant .generic {}
pattern with some global utilities mixed in.
I’m not anti-BEM nor anti-roboclasses, but as we enter a new era for CSS I think we have an opportunity to rethink best practices around architecting CSS. Before we get lost rethinking the wheel, let’s hold on to some good principles from the past decade or so:
- We want to author in components
- We generally want low-specificity to avoid collisions
- We want a bucket (sometimes a very large bucket) of global utility classes or variables for ad-hoc composition or customizations
With those principles, let’s dive into some CSS architecture alternatives.
CUBE
When Andy Bell introduced CUBE, I instantly gravitated to it. CUBE is basically the style of CSS I write but with a little more structure and language around it.
- Composition
- Utilities
- Block
- Exception
One nice thing about CUBE is that it’s less prescriptive about how you author “blocks” (read: components). You could totally use BEM inside that zone if you wanted. I like how CUBE embraces the cascade, utilities, components, and one-off exceptions that always drag a codebase down over time. CUBE has a “real world” quality to understanding how CSS degrades over time.
HECS
Let’s go all-in on the components angle. Did you know the web platform has a native built-in to control style collisions!?! In Web Components you get component-level scoping right out of the gate with Shadow DOM1 and you may find yourself writing CSS like it’s the year 2000 again!
:host element .class:state { }
/* the `:host` part is really just here for the acronym */
Be as specific as you want, because the CSS never escapes the :host
component. If the whole reason you were writing BEM or preprocessing with some other scoping mechanism was to avoid specificity collisions, the Shadow DOM eliminates that need.
The tradeoff here is Web Components eject from the cascade2. That means no global utility classes unless you import or adopt them into your component. CSS variables operate in a strange land of inheritable styles that do pass through the Shadow DOM, but not all Web Components support CSS variables, so your ability to control style gets limited by the components you use or the amount of Light DOM in your components.
If you want to learn more about isolated styling in the Shadow DOM, check out my Web Components course on Frontend Masters.
WILS
If your goal is to reduce specificity, new native CSS tools make reducing specificity a lot easier. You can author your CSS with near-zero specificity and even control the order in which your rules cascade.
- :where() # Zero specificity
- :is() # Specificity is highest selector in group
- @layer # Control order styles apply
- @scope # Control when styles stop and start applying
The new’ish :where()
and :is()
selectors give us some incredible de-specifying tools. Tools like @layer
(available now) and @scope
(coming soonish?) give us new powers to control the application of CSS rules. @layer
can be setup to control what order styles apply and @scope
controls where styles start and stop applying.
Over-abstracting in my head a bit, WILS is probably best on a global utility layer or base component library where you need to de-specify as much as possible.
GPC
What about going all-in on controlling order of application? CSS Cascade Layers are like “folders for CSS” where you add rules to those layer folders and specify the order the layers apply.
In 2012, Chris Coyier wrote a post called One, Two, or Three to answer the question of “How many CSS files should there be on a website?”. He came to the conclusion that “three” sounds about right: Global styles, Page level styles, and Section-specific styles. I feel like this technique still works and with some modernizations, we end up with something like this:
@layer global, page, component
@layer global {
// reset goes here
// utilities go here
}
@layer page {
// about page layout goes here
}
@layer component {
// component styles go here
}
@layer page {
// oops, more page styles go here
}
@layer component {
// oh yeah, one more component I forgot
}
All your global CSS goes in the global layer, page-specific CSS goes in the page layer, and component (section-specific) styles go in the component layer. You can author layered styles anywhere and they’ll organize themselves in the correct order. Amazing!
We can get weirder I’m sure, but this is a nice baseline for me I think.
Other acronyms on the horizon
I’ve presented some ideas and riffs, some are good and some are bad, but the whole point of this post is that there’s heaps of new tools that might reshape how we write CSS. I’m excited to read about new organization systems and how others architect their CSS in this new styling reality.
For example, the other day I dug into Miriam’s CSS setup on her new site, oversimplified, it looks a bit like this:
@layer spec, browser, reset, default, features, layout, theme
This system demonstrates how styles apply from the spec text to the browser all the way to the custom theme layer. You can select your level of enhancement and roll back the fidelity of her site if you desire. It’s clever, illustrative, and very on-brand for the person who wrote the Cascade Layers spec.
While SBRDFLT doesn’t exactly roll off the tongue (”S-Bird Felt”?), maybe it’s time we break out of catchy three letter acronyms and focus on finding good scalable architecture patterns instead. I look forward to seeing what systems people come up with in this new era of CSS. Bonus points if you blog about it and explain the problems you’re trying to solve.
Edit: Previous versions of this article said @layer
and @scope
where high specificity. Updated per feedback