Understanding CSS: Selector Specificity
Let me throw you a scenario you may be very familiar with.
You’re working on a feature for a website or web app that requires some overriding or other changes to the style of a component. You jump into Web Inspector, grab the class for the element(s) in question, and write some new CSS. Easy. However, after refreshing the page, none of the changes have been made — or some have, but not all of them. Maybe the color changes, but the `margin-left: auto` you gave the element remains the same.
This is usually because of specificity. CSS specificity refers to the specificity of the conditions of a CSS selector. Here’s an example:
This selector has a low specificity, since it’s just targeting an `a` element. Let’s increase the specificity.
This CSS is targeting an `a` element inside any element with a class of `modal`. We could add a condition to the class selector, too:
Now the CSS is targeting `a` elements inside `div`s with a class of `modal`.
The more conditions there are in a CSS selector, the higher its specificity. Specificity trumps the cascade, so in this case:
Even though the rule on line 5 appears later in the stylesheet, its specificity is not as high as the rule on line 1, so `a` elements inside `div`s with a class of `modal` will appear green.
How CSS Selectors Are Read
Before we go much further, it’s worth understanding how CSS selectors are read and understood by a web browser. Contrary to many people’s expectations, CSS selectors are read from right to left. Given the following selector:
The browser would query every DOM element on the page to find its target, like so:
- Begin target search
- Find every `a` element on the page
- In that subset, find elements that are a descendant of an `li` element
- In that subset, find elements that are a descendant of a `ul` element
- In that subset, find elements that are a direct descendant of any element that has a class of `site-nav`
- In that subset, find elements that are a descendant of a `header` element with an ID of `banner`
- In that subset, find elements that are a descendant of any element with an ID of `home-page`
- In that subset, find elements that are a descendant of an `html` element
- Apply styles to target
This may seem like an extreme example, but with SASS, it’s easy to end up with selectors of this complexity and specificity.
Let’s rewrite that rule with a smart, BEM-style class to select the same element.
The target search now looks more like this:
- Begin target search
- Find every element on the page with a class of `site-nav__link`
- Apply styles to target
By using a single class selector with no nesting:
- Target search is reduced from 9 steps with vast DOM traversal to just 3 steps
- We’re greatly reducing the number of conditions required to change the style of the target
We’re also helping future developers understand the code that we’re leaving behind — the BEM notation indicates that we’re styling a `link` element inside the `site-nav` block/context.
Calculating CSS Specificity
Now that we understand the implications of selector specificity, let’s get some hard numbers to help us diagnose specificity issues.
People smarter than myself managed to come up with a numerical representation of CSS specificity, allowing us to calculate specificity scores. It works out like this:
- Elements and pseudo-elements (`a, div , body, :before, :after`) get a score of 1
- Classes and attribute selectors (`.element, [type=”text”]`) get a score of 10
- IDs (`#header`) get a score of 100
- Inline styles get a score of 1000
- `!important` gets a score of NaN (it’s more “specific”/powerful than even inline styles, and can only be overridden by the cascade — writing rules later in the stylesheet)
Let’s calculate the score for one of the earlier examples.
This selector has:
- 5 elements (5)
- 1 class (10)
- 2 IDs (200)
Giving it a total specificity score of 215. You’d have to write a rule of equal or higher specificity to override the styles applied by it.
With the scoring in mind, take a look at the specificity graph for Dropbox’s `main.css` here. You can see the graph spikes at a score of 532. That means a CSS selector that looks something like this:
Bear in mind this is the specificity of an actual selector in Dropbox’s CSS today. Yikes.
Deferring Specificity: Mountains and Valleys
Take another look at that specificity graph. Notice how its peaks are scattered across the X-axis. The X-axis is a representation of progress through the stylesheet, from top to bottom — line 1 to EOF — down the cascade.
In many websites, and Dropbox is no exception, as new styles are needed, they are appended to the end of a stylesheet. This is one reason why these peaks in the graph appear scattered across the graph. But what that means is that when a new “peak” (an overly-specific CSS selector) is added, it takes an even higher peak to undo it. You end up creating a specificity graph full of mountains and valleys, with mountains increasing in size for each time you need to override a previous rule.
A preferable structure for CSS would be to defer more specific selectors to the end of a stylesheet, so that the specificity graph more closely resembles a steady incline. This means fewer mountains to climb, and fewer valleys to get lost in.
Harry Roberts, a UK-based front-end architect, has a great talk on how to structure CSS in a way that defers specificity:
Avoiding Over-Specific CSS
Avoiding writing overly-specific CSS rules takes just being mindful of a couple of things.
- Avoid nesting
- Opt for namespaced BEM classes
- Aim for low specificity when increasing specificity is a requirement
Understanding how CSS specificity works will make those things much easier, and if you made it this far, you should have a pretty good understanding by now!