On your average CSS-writin’ day, odds are you won’t even think about precedence in CSS. It doesn’t come up a whole heck of a lot. But it does matter! It comes up any time multiple CSS selectors match an element with the exact same specificity.
Assuming specificity is exactly the same, order does matter.
Styles declared later win.
Within a single stylesheet
Say we have some HTML like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="module module-foo module-bar">
Module
</div>
</body>
</html>
The order of the attributes in the HTML don’t matter. It’s the order in the stylesheet. Let’s focus on the background
:
/*
All of these selectors match
and they all have the same specificity
*/
.module {
background: #ccc;
padding: 20px;
max-width: 400px;
margin: 20px auto;
}
.module-foo {
background: orange;
}
/* LAST declared, so these property/values win */
.module-bar {
background: lightblue; /* I win! */
/* I still have all the _other_ styles as well */
}
An intentionally convoluted example
Order is not limited to a single stylesheet. The order of the stylesheet in the document matters even more.
Check out this document with three distinct style… uh… let’s call them chunks. A chunk being either a <link rel="stylesheet">
, a <style>
block, or an @import
ed stylesheet.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<!-- #1 -->
<link rel="stylesheet" href="1.css">
<!-- #2 -->
<style>
.module-baz {
background-color: pink;
}
</style>
</head>
<body>
<div class="module module-foo module-bar module-baz">
Module
</div>
<!-- #3 -->
<style>
@import "2.css";
/*
Contains
.module-bar { background: #f06d06; }
*/
</style>
</body>
</html>
I labeled the chunks #1, #2, and #3. All of them contain CSS selectors with the same exact specificity. #3 is the last declared, so it wins the precedence battle.
Async loaded CSS still respects document order
Perhaps you’re loading CSS with an awesome CSS loader like loadCSS. What happens if we were to load a fourth CSS file with it with the exact same setup as the “convoluted” example above?
loadCSS injects the stylesheet at the bottom of the <head>
by default, so it would become #3 and the <style>
block at the bottom of the body would become #4 and thus win.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="1.css">
<script src="loadCSS.js"></script>
<script>
loadCSS("2.css");
</script>
<!-- 2.css will be injected right here -->
</head>
<body>
<div class="module module-foo module-bar module-late">
Module
</div>
</body>
</html>
It’s actually invalid (although it works) to have a <link>
or <style>
block as a child of the <body>
, so it would be really rare for a stylesheet loaded by loadCSS to not be the winner by default.
Also, you can specify an ID to target so you can control CSS order:
<script id="loadcss">
// load a CSS file just before the script element containing this code
loadCSS("path/to/mystylesheet.css", document.getElementById("loadcss"));
</script>
Does Critical CSS get weird?
One of the reason you might use loadCSS at all is because you’re intentionally trying to defer loading of your stylesheet, because you’re injecting critical CSS into the <head>
to try and get styles into the first packet and speed up rendering.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
/* #1
Critical CSS chunk up here */
</style>
<script>
/* #2
Load the rest of the CSS
*/
</script>
</head>
<body>
<div class="module module-foo module-bar">
Module
</div>
</body>
</html>
The practice of critical CSS involves moving up CSS selectors into a higher chunk. The #1 chunk. The lowest-order and easiest-to-override chunk. So, theoretically, yes, there could be conflicts/changes in what CSS gets applied when comparing the page with just the critical CSS applied and with the CSS fully loaded. But the stylesheet does fully load, and comes after the critical CSS, so it will ultimately be as-intended.
You might might need to fine-tune exactly what makes it into critical CSS and what does not – to avoid weird flash-of-style-changes.
Do extends get weird?
In a preprocessor, they can.
Say you want to style a thing with a variation:
<div class="module module-variation">Module</div>
And the (super simplified for demo purposes) preprocessor code ends up like:
%variation {
background: orange;
}
.module {
background: #ccc;
padding: 20px;
max-width: 400px;
margin: 20px auto;
}
.module-variation {
@extend %variation;
}
You’d think… OK, .module-variation
is the LAST declared selector, so it wins, so the background should be orange. But it’s not, because the extend moves the selector to where the thing it’s extending is defined, which in our case is before what we are trying to override. The compiled CSS is:
.module-variation {
background: orange;
}
.module {
background: #ccc;
padding: 20px;
max-width: 400px;
margin: 20px auto;
}
So the background
is actually #ccc
, not what we want. Probably easiest to solve this with specificity rather than source order.
Native extends, should they become a thing, would presumably be less confusing.
It’s a silly thing to manage
Nobody wants to think about this. Winning style with specificity is way easier. But knowing about it is a good idea, because unnecessary specificity bloat is a bummer too.
And on we dance.
I love how this is really an expert article. I am interested about education, have taught to many people HTML+CSS and was having a conversation the other day about the expert way of explaining things here:
Which is the basics of CSS… Cascade and all. It is the basic thing to explain to someone learning, that if you write a different style later on it overwrites the previous one.
But once you dig deep into it, maybe even try SASS, BEM or others, then you don’t really work with the cascade anymore. That is why sometimes you need a reality check. Also of course there is all the special cases mentioned here.
Using sass or bem makes no difference for specificity – it still gets used bud.
Actually, using BEM gets you working a lot more with precedence than specificity, since it’s main goal is to reduce specificity.
For the last section about
@extend
,there is another useful CSS spec for that:
CSS @apply Rule https://tabatkins.github.io/specs/css-apply-rule/
And we can use
@apply
on Chrome Canary now!Here’s a good article for the topic:
One thing to note is that embedded styles always win! (with same specificity of course)
I didn’t realize that thing about the preprocessor changing order. Good to know!
Do you remember that article who said “don’t use background instead of background-color”?
The thinking behind that is: it resets things you might not be intending to reset (e.g. background-repeat). Being aware of that is the important thing.
thanks author
Regarding links and styles in body, see:
https://jakearchibald.com/2016/link-in-body/
This is discouraged, but not “invalid” as you suggest here. Apparently.
Looks like Chrome will move towards the IE/Edge behaviour, where progressive rendering of content will be supported. Stylesheet links will block below, but not above.
My “invalid” basis was based on what https://validator.w3.org/ says at the moment, which probably isn’t super duper up-to-date.
Hi there, good and exciting article, but I have a doubt related to the changing order in the css compile process… What about if you are extending one more class? is it taking the one which is more at the top? Is it respecting an order related to the extending classes order?
Thanks!
Extending a placeholder will always put the selectors ate the placeholder position, so it does not matter how many selectors there are. It will always work as Chris explained.