As I was building out Innerhelm, I made some CSS organization decisions that, so far, I’m quite happy with. I recently reorganized my personal site’s styles to follow the same pattern. I thought I would explain it and its rationale in a post.
I have five CSS “layers.”1 Listed in order of increasing specificity:
- The theme, as CSS custom properties applied to the
:root - The base styles—styles that apply to raw elements, e.g.
pandh1. - Layout classes, based on Every Layout, prefixed
with
l-. - Utility classes, prefixed with
u-.2 - Component-scoped styles (via Astro), with attribute-based scoping (which increases their specificity above the other layers).
Each of the first four layers has its own SCSS source file.
For an example of how this looks in practice, check out the home page’s source Astro file.
I like this organization a lot, for a few reasons:
-
I can immediately tell where a given class is defined. It has a prefix? It’s a layout or utility, depending on the prefix. It doesn’t? It’s a component scoped style and I just need to scroll down.
-
I can easily jump to the relevant source files. For example, to go to the source file for
u-patternin VS Code, I just doCtrl+P, "util", Enter. -
Selectors with higher reach have lower specificity. This concept was formalized by Harry Roberts as ITCSS, and also appears in the CUBE CSS methodology I used previously. This makes specificity easy to manage and avoids specificity fights or the need for overrides like
!important. Also, following this rule has a side-effect of making it easy to inspect and tweak things via the CSS inspector in my browser’s Dev Tools. -
It’s powered by CSS custom properties, which makes it easy to change design tokens to tweak the whole site at once.
-
It’s compositional, while still fully embracing the strengths of component-scoped styles. Component-scoped style classes are “first-class citizens”—any class without a prefix is a component-scoped class.
Using component-scoped styles solves all the problems that frameworks like Tailwind claim to solve, without the crufty HTML and driving-a-car-from-the-outside vibe. I can use the full expressive power of CSS, embracing the cascade instead of treating it like a quirk to be patched over.
-
Because every class that’s used across multiple files has a prefix, it’s easy to find the usages of that class. I can find all usages of
u-patterneasier than I could find usages ofpattern—the latter would have many false-positive search results in my codebase.(I could possibly use an extension like CSS Navigation or CSS Peek to eliminate the false positives, but, from my understanding, those would fail to find usages that don’t occur directly in HTML, such as a class that is dynamically chosen in the component script of an Astro component.)
I know what you’re thinking: “But does it scale?”
Good question. I don’t know yet. Scanning over Chris Coyier’s Scalable CSS scorecard, it seems like it would meet those requirements, but given that I’ve only used it on these two comparatively-small sites so far, I’m not sure. I’ll update this post if my thoughts on its scalability change over time.
Also, before anyone asks, I’m intentionally not going to give this organization an acronym, because I want to avoid this thing that happens. These patterns are just what works for me.3
Footnotes
-
These are only conceptual layers—they don’t use the new CSS Layers API. Maybe they should? I haven’t dabbled with Layers yet so I’m not too sure how they work, or if Astro’s component scoped styles could be put into one. ↩
-
Technically these are the same specificity as the layouts, but I import their source file after importing the layout file, so that they occur later in the source order and thus take precedence. ↩
-
On Innerhelm I also use a full Utopia-powered responsive space and type system, but I haven’t added that here yet. I just have a few responsive type sizes. ↩
Comments
0 comments
0 replies