My attitude toward CSS used to be that it is simply unmaintainable. If in the course of development, you think things like:
- These styles are already a mess, so I can’t make it any worse.
- I can’t figure out why this is getting overridden, so I’ll just use
- It seems like these styles are unused, but I don’t dare remove anything.
…then you know what I’m talking about.
Two of the most prominent “features” of CSS are also two of the worst practices in software development: globals and inheritance. But whereas in most of software development, you have to choose to make things global or to use inheritance, with CSS, you don’t have a choice. Everything is global, and inheritance is at its core (it’s right in the name, cascading style sheets).
So if you have to contend with CSS, what can you do to make it more manageable?
There are some efforts toward abstracting it away completely, like CSS in JS, which eliminates the cascading and sheet parts of CSS, leaving just style. But chances are good that you still have to deal with CSS itself most of the time, which gives you a few choices:
- Complain about how terrible CSS is and dwell on how much you hate it.
- Stick your head in the sand and pretend that CSS is great.
- Give in to the hopelessness and despair that CSS will always be a mess, so why even try?
- Acknowledge that CSS has more bad parts than good parts, but do what you can to focus on using the good parts and minimizing the bad parts.
The Good Parts
The best part of CSS is the ability to apply styles using a consistent syntax, separate from the document structure. But we don’t have to work directly with CSS to experience this benefit. Fortunately, CSS preprocessors have been around for a while, and they provide many of the features that are missing from CSS (like constants). Using a preprocessor is an essential first step toward better CSS. Sass is a popular choice, but there are lots of options, so find what works for you.
After that, focus on minimizing the bad parts.
Since CSS doesn’t have a scope other than global, it’s critical to have some naming conventions. Whatever you come up with, document it. It’s all for naught if future developers (or current teammates) don’t know what the rules are and why you’ve chosen them.
The goal is to establish some kind of hierarchy that keeps the number of names at each level manageable and avoids collisions among all names. If your application can be broken down into components/modules, then taking a component-based strategy to naming is a great start. Make the top level of your naming hierarchy match the names of those components or modules, and you’ll have no problem with naming collisions. From there, use a naming convention like BEM. It may seem overly verbose at first, but you’ll soon realize that’s a small price to pay for the tremendous gain in clarity.
Styles for things that don’t fit neatly into the component structure can be provided using utility-based classes. These are more general-purpose, reusable styles that often have more to do with layout than styling. These should be used a last resort, though, as you can easily end up with a pile of classes like
<div class="row header primary-green bordered bottom-margin-large">.
app-, can quickly identify a class as one that has no associated styles and is only meant for scripting.
Ultimately, it should be easy to tell which category a classname falls into when examining HTML. The browser’s inspector can be used to figure out what styles are being applied for each class, but that’s like stepping through code using a debugger (necessary sometimes, but generally slow and tedious).
Inheritance and Specificity
Context-sensitive anything is harder to reason about than an equivalent context-free thing. For example:
|A statement that is heard out-of-context||A statement that needs no context|
|A function that operates only on the arguments it was given||A function that pulls in state from elsewhere, like an instance variable|
|A CSS selector involving multiple elements, like
||A CSS selector that matches based on a single element|
The advantage of context-sensitive rules is that they can save time upfront, allowing you to spray styles all over the place with fewer rules. This is fine if you need to get a prototype working quickly, but it’s not so great if you’re writing something that needs to be maintained.
Inheritance is difficult to reason about because it tightly couples classes. A change in one can cause unintended changes in the other. And CSS is built on multipile inheritance, driven by a system of specificity. The choice of whether or not to apply a rule depends on how specific its selector is.
This usually leads to more and more specific selectors in an attempt to override previous styles, which also binds the rules more tightly to a specific document structure (and therefore a very specific context). Components are easier to test and easier to reuse when they’re not bound to a particular context.
It’s easy to get carried away trying to reduce duplication of styles by using general-purpose classes. But there tend to be just a few styles that truly need to be shared throughout an application–things like color palette, fonts, and sizes.
Any CSS preprocessor worth using supports variables and mix-ins, which can be used to define these shared styles. Just be careful with the mix-ins, or else you might end up creating a whole new inheritance problem.
With variables (constants really) and a few mix-ins defined, component-based style rules can then opt in to these styles. This way, when looking up the style definition for a component, you can clearly see all of the rules that it wants to be applied.
Contrast this with traditional CSS, where styles can come from anywhere. A selector like
.parent > .child is not really defining
.child, but rather overriding it when
.child is a direct descendant of
When I focus on utilizing only the good parts of CSS, it feels like I’m throwing out 90 percent of it. But if that means I can spend less time debugging bizarre style bugs, I’ll do it gladly.