Once you’ve committed to writing surgical CSS that only targets what it needs to, you begin to run into another quirk.
CSS is inherently global—every rule in your stylesheet is evaluated by the browser in relation to the entire site. It’s up to you to wrap your rules in enough selectors to narrow down the impact.
And while CSS gives you tons of flexibility, there are hidden traps waiting for you. Consider this puzzle: can you guess, at a glance, which color this button ends up being?
.cta a.button {
background-color: red;
}
.site-container .button {
background-color: green;
}
#cta-button {
background-color: blue;
}
.button {
background-color: yellow;
}
If you guessed red, green, or yellow, you lose. If you guessed blue, you also lose, because nobody wins with code like this.
Every combination of selectors you use to whittle down your CSS rules can have different effects on specificity, or which rule will win out when multiple selectors could apply to a given element.
There are some techniques you can follow in new projects that avoid what Nicole Sullivan calls a “hostile code environment”—a project with such specific CSS rules that it’s difficult to add to or even understand quickly. Pity the poor soul who later has to turn our button above purple and add to the top of the Jenga tower of selectors…
Style by Class, Not by ID or Tag
Classes are the most flexible of CSS selectors: they can apply to any element, elements can mix and match multiple classes, and there’s no expectation that a class is unique on a given page.
Selecting by ID is ten times more specific than class name, which is ten times more powerful than element name. By only ever styling by class, you avoid accidentally creating overly-specific rules that make subsequent overrides harder.
// BAD:
#myid.widget-a {
// Specificity: 110
}
div.widget-a {
// Specificity: 11
// Whoops, this selector isn't specific enough to override!
}
.widget-a {
// Specificity: 10
// Whoops, this selector isn't specific enough to override!
}
// GOOD:
.widget-c {
// Specificity: 10
}
.widget-c {
// Specificity: 10
// Wow, super easy to override styles later on!
}
This levels the playing field of specificity highlighted above, ensuring that you only have to make your selectors minimally specific to override a style.
Flatten the Hierarchy, Power to the Components
Another powerful tip is to carefully consider and limit your use of the descendant combinator. To those other than CSS-spec-reading-geeks, this is when you combine selectors with a space, changing how elements appear when they’re inside other elements.
// Default styles
.cta-button {
font-size: 14rem;
color: blue;
}
// A special case where we wanted the button to
// change inside of some promos.
.big-promo .cta-button {
font-size: 20rem;
color: red;
}
// Now the homepage wants a different color button
// on its big promo. Whoops, we're going to have to
// include .big-promo in the selector to make sure it overrides!
.page-home .big-promo .cta-button {
color: green;
}
This example is just the start of the problems. What happens when you want to reuse the same style somewhere else? Or override in an even more specific scenario? Just Add More Selectors™?
You should strive to keep as few levels of descendent selectors going on as possible—style things based on what they are instead of what they’re inside of. This not only helps avoid specificity wars, but also helps you think of your styles as collections of reusable components, not just loosely-related variations of styles painted on a pixel-perfect page.
// Default styles
.cta-button {
font-size: 14rem;
color: blue;
}
// Modifier classes (these are in addition to
// the default class).
.cta-button-big {
font-size: 20rem;
color: red;
}
.cta-button-homepage {
color: green;
}
In the improved example above, we’re using only one level of class to describe both the default and two different overrides. Now, not only are future overrides easier, but we also gain some benefits of clarity—when looking at the CTA button’s CSS file, we see at a glance how this component can change, and these changes are “opt-in” by applying classes in the HTML instead of unexpectedly applied when the component is included on a specific page. Of course, there are still instance in which the descendent selector makes sense, but using it judiciously will make things less frustrating.