The Specificity Siege: why your CSS is ignored, and why !important makes it worse
Your button's CSS loads last and still loses. Kitsu, the fox spirit who guards Domlok's CSS, weighs the selectors, explains specificity, and lowers the heavy one with :where() or @layer so the Important Demon never moves in.
Told by Kitsu, with Kai asking the questions
Kitsu: Your rule isn't wrong, Kai. It's just lighter. In CSS the heavier selector wins, and loading last only settles a tie.
If a CSS rule you just wrote is ignored even though its file loads last, another rule that matches the same element almost certainly has higher specificity. Source order only decides between selectors with exactly the same score. Lower the heavier selector and your rule wins, with no !important anywhere.
In the world of Domlok this is the work of the Important Demon, an oni whose horns are two exclamation marks. It never breaks your build. It waits until you get frustrated and type !important, and then it moves in for good: every rule after that needs one more !important to win.
Your guide for this episode is Kitsu, the Fire guide of Domlok and the fox spirit who looks after CSS and layout. She can weigh any selector at a glance, which makes her the right one to end a specificity siege. Kai, as usual, is the one who summons the demon.
The button that ignored Kai
Kai's site header greys out every navigation link. Then he adds an orange sign-up button to the same nav, in a stylesheet that loads after the header's:
Docs Sign up
#site-header .nav a {
color: #5f564b;
}.btn-cta {
color: white;
background: #d9693f;
}The button turns orange, but its text stays grey. The background applies because nothing else sets one. The color doesn't, because two rules want to set it on the same link.
KaiIt's the last file. It says white. Why is it still grey?
Weigh the selectors
When two declarations from the same stylesheet origin and cascade layer compete, the browser compares their selectors' specificity. It is written as three numbers, ID-CLASS-TYPE:
| Column | What counts | Examples |
|---|---|---|
| ID | ID selectors | #site-header |
| CLASS | Classes, attribute selectors and pseudo-classes | .nav, [type="email"], :hover |
| TYPE | Element selectors and pseudo-elements | a, p, ::before |
| Nothing | The universal selector, combinators and :where() | *, >, +, space |
So #site-header .nav a scores 1-1-1 and .btn-cta scores 0-1-0. The browser compares the columns from the left, and the first column that differs decides. One ID beats any number of classes, and one class beats any number of element selectors.
KitsuOrder only breaks a tie. If both selectors scored 0-1-0, the later one would win. Here the header rule is heavier, so it wins wherever it sits.
Two more things that trip people up. Inline style attributes beat every selector in your stylesheets. And :is(), :not() and :has() add nothing themselves but take the specificity of their most specific argument, so :is(#app, .card) scores like an ID.
Kai summons the demon
.btn-cta {
color: white !important;
}It works. An !important declaration beats normal declarations whatever their selectors. Then the next week Kai needs a muted version of the button:
.btn-cta.is-muted {
color: #e7dcc8; /* ignored: white !important wins */
}
.btn-cta.is-muted {
color: #e7dcc8 !important; /* now this one wins */
}The muted rule has the higher score (0-2-0), but a normal declaration can't beat an important one. So Kai adds a second !important. When two important declarations compete, specificity decides again, and the siege starts over one level up. A month later his CSS has a dozen of them and nobody dares to delete one.
KaiSo every new rule needs one more !important. That's not a fix, that's a war.
The fix: make the header quieter
The button is fine. The header rule is too heavy. Bring it down and the button wins on its own.
Option 1: wrap the heavy part in :where()
:where(#site-header .nav) a {
color: #5f564b; /* 0-0-1 */
}.btn-cta {
color: white; /* 0-1-0: wins */
background: #d9693f;
}
.btn-cta.is-muted {
color: #e7dcc8; /* 0-2-0: wins when muted */
}Everything inside :where() counts as zero, so the header rule still only matches links inside the nav but now scores 0-0-1. The same idea works without :where(): give the nav links their own class, such as .nav-link, and drop the ID from the selector.
Option 2: put the stylesheet in order with cascade layers
@layer base, components, utilities;
@layer base {
#site-header .nav a {
color: #5f564b;
}
}
@layer components {
.btn-cta {
color: white;
}
}Layers are compared before specificity. A rule in a later layer beats a rule in an earlier layer even if its selector is lighter, so the ID in base can no longer bully a class in components. Two cautions: styles that are not in any layer beat all layered styles, and !important reverses the layer order, so an important declaration in base would beat one in utilities.
KitsuUse :where() to fix one heavy selector today. Use layers when a whole codebase keeps fighting itself.
Spot the siege in DevTools
- Right-click the element and choose Inspect.
- In the Styles pane, find the property that isn't applying. The losing declaration is crossed out, and the rule above it is the one that won.
- Hover over the winning selector. Chrome DevTools shows a tooltip with its specificity, for example
Specificity: (1,1,1). - Open the Computed tab to see the value the element actually ends up with.
If the Styles pane is full of !important, the demon has already moved in. Find the heaviest selector in the fight, lower it, and delete the !important it made necessary.
KaiWhite text, a muted state that works, and zero !important. The demon's gone quiet!
Before you reach for !important
- Which other rule matches this element and sets the same property? Check the crossed-out line in DevTools.
- What are the two scores? Compare ID, then CLASS, then TYPE.
- Can the heavier selector lose its ID, or move inside
:where()? - Would a cascade layer make the order explicit for the whole codebase?
- Keep
!importantfor the rare rule that must always win, such as a utility like.visually-hidden, never to win a fight you don't understand.
Acharya's Rule #9Keep selectors flat. !important starts a siege, it never ends one.
Next episode: The Thrashing Golem. Why reading layout values like offsetHeight inside a loop can freeze your page, and how batching reads and writes sets it free.
Think you can banish the demon yourself? Play Banish the Important Demon on domlok.in and earn seal 09.
Watch the Short and the full episode on YouTube at @dom-lok and save the carousel on Instagram at @mbd.domlok.