By Harry Roberts
Harry Roberts is an independent consultant web performance engineer. He helps companies of all shapes and sizes find and fix site speed issues.
Written by Harry Roberts on CSS Wizardry.
One thing I believe, as a very, very general rule of thumb, is that as sites get bigger, selectors should get shorter.
By this I mean that if you want to create extensible and maintainable, flexible and predictable websites, you should really take care to make your CSS selectors as dev-friendly as possible; i.e. short.
Keeping CSS selectors short helps with a lot of things:
This is a very vague list, so I’m going to address each in order. You will find that there is a lot of crossover between each point (e.g. reducing location dependency inherently means your selectors are more portable) but I feel they are all points in their own right.
I have written before about CSS selector efficiency. I’m going to gloss over a lot of the intricacies in this post so for a full background understanding I recommend you read Writing efficient CSS selectors first.
If we ignore actual types of selector (* {}
is typically the slowest, depending on how it’s being used, IDs are the fastest followed by classes, descendants are comparably quite slow followed by pseudo-selectors) then in general it is safe to say that shorter selectors are faster.
This stands to reason, if we compare these two selectors:
html body .header .nav {}
.nav {}
There we can see pretty clearly that in the first example, the browser has to look out for four things, the .nav
class, then the .header
class, then the body
element and then, finally, the html
element (browsers read selectors right-to-left).
With the second example the browser only needs to look for one thing; the .nav
class. The browser has four times less work to do to match that selector. Every time you write a selector try and trim as much losable stuff from it as possible. Instead of ul.nav {}
(two checks) write .nav {}
(one check). Instead of .nav li a {}
(three) write .nav a {}
(two).
Now, CSS selector performance is—by-and-large—not something we really need to worry about any more, but that doesn’t mean we should be wasteful. I’m sure none of us would miss a lost £5 but that doesn’t mean we go slipping banknotes into paper shredders… Selector efficiency does exist and you might as well improve it where you very easily can.
By keeping selectors short you are likely to be reducing the amount of descendant (e.g. .sidebar .promo {}
) and child (e.g. .sidebar > .promo {}
) selectors. By removing these descending types of selectors you are reducing the necessity for an element to live inside another one. Let’s reuse the .sidebar .promo {}
example…
By having a selector like .sidebar .promo {}
we are saying we want to target any promotional item that lives in an element with the class of .sidebar
. This means that we are tied to always using that styling inside a certain element; we have a dependency on location.
By replacing .sidebar .promo {}
with something like .secondary-promo {}
we can now place the element in question anywhere we wish. In the sidebar—as before—but now also in the footer, or in the header, or after an article.
By reducing descendants we can really reduce dependency and make things a lot more portable…
So now that we’re not tied to locationally dependant selectors, we find that our components are a lot more portable. We can move things a lot more easily because our CSS doesn’t care where a thing lives, it just cares that it exists. Awesome!
Another way to increase portability is to not qualify selectors. A qualified selector is one like ul.nav {}
or a.button {}
or div.content {}
.
Qualified selectors are bad because they reduce efficiency (more checks than we really need) but—more importantly—because they tie us to specific elements. We can’t now use that .button
class on an <input>
or a <button>
, for example. We can’t apply .nav
to an <ol>
to make a breadcrumb.
Selectors should be element-agnostic. Your CSS shouldn’t care what element you’re wanting to apply styling to.
Another way to make selectors more portable is to drop elements altogether. Take this, for example:
/* Base widget styling */
.widget {}
/* Style up widget titles */
.widget > h2 {}
Here we have a troublesome selector; what if that <h2>
needs to become a <h3>
? What if we need to add another, non-titling <h2>
as a child of .widget
? We’ve made ourselves a very rigid and unportable selector here. Instead we should have:
/* Base widget styling */
.widget {}
/* Style up widget titles */
.widget-title {}
Now we can apply .widget-title
to any element—let’s say a <h4>
—and can now also have any number of unclassed <h4>
s in the widget without them adopting any title styling. Ossom!
The longer a selector is, the more things the browser has to satisfy before it can match it. The more checks there are then—naturally—the more chance there is for something to go wrong.
A (very exaggerated) selector like body > div:nth-of-type(2) > article:first-child > p:first-child {}
—borrowed from my talk Breaking Good Habits—has ten checks; ten things that must be satisfied in order for the browser to make that match.
All that needs to happen is the location of the div:nth-of-type(2)
to change or the p:first-child
to become a blockquote
or the article:first-child
to no longer be a child of the div:nth-of-type(2)
or any manner of things before that selector will break. Simply replacing that with a class of .intro {}
means that there is only one thing that could possibly break, and the chances of that happening are pretty much zero (you’d have to explicitly delete the class from your HTML to prevent a match).
Shorter selectors mean there is statistically less chance for things to go wrong.
This is the big one! This is where it really matters!
Longer selectors have a higher specificity. Specificity is a nightmare and you should keep specificity as low as possible all of the time. We already know that we do not use IDs in CSS but a chain of selectors are often just as bad (though not quite).
A selector like .widget > h2 {}
has a higher specificity (as well as the other problems we discussed) than a selector like .widget-title {}
.
.nav li a {}
has a higher specificity than .nav a
(and is also less efficient). Reducing selector length reduces selector specificity and that is very important. High specificity leads to self-induced specificity battles that can only be won by making subsequent selectors more specific (or using !important
, shame on you). This is a terrible thing. The easiest way to reduce specificity (after dropping IDs from your CSS completely) is to keep your selectors short.
This is a very specific but very decent example of how short selectors can make code more forgiving. However, I will warn you, you can argue two sides of what I’m about to tell you; you can argue that it makes your code a lot more flexible and can gracefully handle breakages or you could argue that it allows breakages in the first place by being too lenient. Anyway, here’s a true story…
In working on a pretty huge project at Sky I stuck to my own rules and coded a (vertical) nav bar CSS like so:
.nav { /* Nav styles */ }
/* Note NO .nav li styles as this was a vertically stacking nav. */
.nav a { display:block; /* More styles */ }
Now, there was a CMS error which went undetected where the markup getting spat out was:
<ul class=nav>
<a href=#></a>
<a href=#></a>
<a href=#></a>
<a href=#></a>
</ul>
Spot the problem? No <li>
s! This is really not cool but, as I had used .nav a {}
instead of .nav li a {}
nothing broke. My code was a lot more forgiving than if I’d had that third check in there.
Now, this doesn’t make the markup right, and it does actually allow poorer markup than a more verbose selector, but you can see how the CSS was very forgiving of a markup error.
Now I said you could argue both sides here, a more verbose selector means that we’d have spotted the CMS error immediately as no styles would have hit the <a>
s. But! In the same breath, our CSS was flexible enough to be okay with that. Make of it what you will, because I too am sat on the fence and a little disappointed that the error wasn’t spotted, but here is a very specific example of how shorter selectors can lead to more forgiving CSS.
I did mention that this is a rule I’ve applied to larger sites but, honestly, you should apply this everywhere. The things we’ve discussed tend to really come into their own (and their absence painfully aware) on larger builds, but they will definitely, definitely help you on builds of all sizes; small or large.
So, by using more classes and less descendants, keeping selectors short and portable, keeping selectors element-agnostic and generally considering maintenance and chance-of-change when writing our CSS, we can really easily improve the quality of our code infinitely. We can make things more efficient, more forgiving, more flexible and more reusable just by revisiting one of the most simple and fundamental aspects of CSS; our selectors.
Harry Roberts is an independent consultant web performance engineer. He helps companies of all shapes and sizes find and fix site speed issues.
Hi there, I’m Harry Roberts. I am an award-winning Consultant Web Performance Engineer, designer, developer, writer, and speaker from the UK. I write, Tweet, speak, and share code about measuring and improving site-speed. You should hire me.
You can now find me on Mastodon.
I am available for hire to consult, advise, and develop with passionate product teams across the globe.
I specialise in large, product-based projects where performance, scalability, and maintainability are paramount.