Skip to the content.

CSS specificity decides who wins, and it is not about order

CSS specificity decides who wins, and it is not about order

When two CSS rules set the same property on the same element, only one wins. Most people assume the last one in the file wins — and sometimes it does, but only as a tiebreak of last resort. The real decider is specificity: a score the browser computes from the shape of each selector. A selector with a higher score beats a lower one no matter where either sits in the stylesheet. This is why a fix you add at the bottom of the file does nothing, and why the frustrated next move — !important — is a symptom, not a solution.

Specificity is a three-column score

The browser reads every selector as three numbers: how many IDs it uses, how many classes / attributes / pseudo-classes, and how many elements / pseudo-elements. Compare the columns left to right, like a version number: a single ID beats any number of classes, and a single class beats any number of element selectors. Order in the file is only consulted when the scores are exactly equal.

Specificity as a three-column scoreboard Three selectors scored in ID, class and element columns. The nav a rule scores 0-0-2, the .active rule 0-1-0, and #logo 1-0-0; the highest column that differs decides, so ID beats class beats element. IDclasselement nav a 0 0 2 .active 0 1 0 #logo 1 0 0 compare left to right: #logo (1-0-0) beats .active (0-1-0) beats nav a (0-0-2)
Read the score like a version number: the first column that differs decides, and a lower column can never make up the gap.

Why your fix at the bottom does nothing

Say a link is styled by its ID, and you try to override the colour with a class you add later:

#logo { color: navy; }        /* score 1-0-0 */

/* added later, at the very bottom of the file: */
.brand-link { color: crimson; }   /* score 0-1-0 — loses, despite coming last */

The link stays navy. The class scores 0-1-0; the ID scores 1-0-0; the ID column differs first and it is higher, so #logo wins and file order is never even consulted. Adding more declarations below changes nothing, because the problem is not position — it is that you brought a class to an ID fight.

Match the specificity instead of escalating

The fix is to make your winning rule score at least as high, then let order break the tie. Raise the new selector to the same tier — here, by scoping the class under the same ID — and it now scores 1-1-0, which beats 1-0-0 cleanly:

#logo { color: navy; }              /* 1-0-0 */
#logo.brand-link { color: crimson; } /* 1-1-0 — wins on the class column */

That is the whole discipline: compute the score, then match or exceed it by one tier, rather than reaching for !important. !important wins by leaving the specificity system entirely, and the moment a second !important appears you are back to comparing specificity among the important declarations, with no clean exit — the escalation just moved up a floor. The durable habit is to keep selectors flat and class-based so almost everything scores 0-1-0 and file order is a real, usable tiebreak. When you do need to win, win by one tier, on purpose. The theme-toggle exercise is a good place to feel this: a theme override has to beat the base rule without an arms race.