It’s easy to let CSS get out of hand and it’s hard to really take advantage of the cascade. Here I’ll explain some techniques that help stem CSS bloat. When you aren’t disciplined with your CSS, it’s easy to end up in a situation where it’s hard to…
- know what parts of the site a particular rule is acting upon
- know whether a given rule is ever used
- be confident your changes won’t clobber something on the other side of the site
- define rules with appropriately specific scopes
- find all the rules acting on a particular element
A well-defined CSS structure addresses some of these problems.
We use one stylesheet named “base” that contains site-wide defaults. Stuff like font sizes, custom fonts, colors, header/footer, and any styles applied to plain HTML tags like a, p, h1-h6, etc.
Then, we use one stylesheet named “widgets” that contains styles for reusable styles. These are more complex than a simple tag, but we want them to be consistent across the site. For example, a comment thread. Each widget is specified with a single class, eg “.comment_thread”, under which all styles for the widget are nested. Only site-wide comment thread styles are in this scope. If your widgets stylesheet gets too big, you can break it up into a widgets folder.
Finally, we have a folder named “/scopes” that contains context-specific styles for different pages. This is where we put anything that only affects a single page, like “article comments are right-aligned” or “don’t show avatars on video comments”. A particular scope stylesheet has all the layout information for a given page and any widget overrides. I usually put a unique class on the body of each page (or maybe combination of two classes, eg controller_name and method_name), then use that as the base for all my scopes.
/* base.scss */
h3 { font-size: 12px }
p { font-size: 10px; line-height: 14px; margin: 1em 0; }
header {
#logo { position: absolute; top: 20px; left: 20px; }
}
/* widgets.scss */
.comment_thread {
.comment {
img.avatar { border: solid 1px #ccc; }
p { margin: 0.5em 0;}
}
}
/* article.scss */
.articles.show {
.comment_thread {
text-align: right;
}
}
/* video.scss */
.videos.show {
.comment_thread {
img.avatar { display: none; }
}
}
I’ve used this structure on large-scale production code. It has made me much more confident I know where any given CSS rule should live, and how specific the scoping should be. It also has helped me know the weight of any given changes. A change to base.css needs much more discussion and review than a change to a scope.
When I was at Contour we moved from a couple monolithic stylesheets to this more granular structure. Fortunately, our site was relatively young and our CSS wasn’t too bad. Even so, we had to use a switch in our <head> tag to load the “new world” vs the “old world.” Replacing the CSS whole-hog wasn’t an option, so we slowly moved pages over to the new paradigm. The first one took forever, but once we had all the defaults dialed in, new pages were much easier to bring over. And honestly, the site looked so much tighter after the revamp. We achieved a consistency across pages that we never had before. It’s easy to justify spending a few hours getting the CSS for a widget just right when they’re truly foundational and reusable.