When CSS “doesn’t work,” it usually is working—you’re just losing a contest you didn’t realize you entered: the cascade + specificity + source order.
This guide is a checklist you can use to predict (not guess) which rule wins, plus the pitfalls that make debugging feel inconsistent.
The 30-second mental model: cascade, specificity, then order
When multiple rules target the same element/property, browsers decide in a consistent sequence.
- Step 1: Importance — !important beats normal rules (but still has its own ordering rules).
- Step 2: Specificity — more specific selectors beat less specific selectors.
- Step 3: Source order — if specificity ties, the rule declared later wins.
If you’re debugging, try to identify which step you’re actually stuck on before changing anything.
Specificity scoring (without the math headache)
Specificity is often described as a score. You don’t need exact numbers—just the ranking buckets.
- Inline styles (style="...") usually win over stylesheet selectors.
- #id selectors beat classes/attributes/pseudo-classes.
- .class, [attr], :hover, :not(...) (see pitfall below) beat element selectors.
- Element selectors (div, button) and pseudo-elements (::before) are the weakest.
One useful shortcut: if one selector has an #id and the other doesn’t, the one with the id almost always wins.
The debugging checklist: predict the winner before you edit CSS
Run this list in order. It keeps you from “fixing” something by accident (like adding another class and making the mess worse).
- 1) Confirm the rule matches the element (typos, wrong class, wrong nesting).
- 2) Check if the property is being overridden by a later rule for the same property.
- 3) Compare selector strength: id vs class vs element.
- 4) Look for !important anywhere in the chain (including third-party CSS).
- 5) Verify the stylesheet load order (your overrides must load after the base styles).
- 6) Watch out for state selectors like :hover, :focus, :disabled that apply only sometimes.
- 7) If nothing explains it, check inheritance and defaults (some properties don’t inherit; others do).
Once you know why a rule loses, you can pick the smallest, cleanest fix.
Pitfalls that make specificity feel “random”
These are the repeat offenders in real codebases.
- Assuming “more words” means more specific. .card .title can lose to #header .title even if it “looks longer.”
- Forgetting that later wins on a tie. Two identical selectors: the one lower in the file wins.
- Overusing !important. It can force a win today and create an even stronger opponent tomorrow.
- Misunderstanding :not(). The :not() itself doesn’t add much, but the selector inside it does. Example: :not(#x) carries id-level specificity because of #x.
- Confusing pseudo-elements and pseudo-classes. ::before is like an element selector (weak). :hover is like a class selector (stronger).
- Mixing component CSS and global CSS. A global rule like button { ... } may unexpectedly lose to .btn { ... }, or vice versa depending on order.
If you keep seeing “I had to add another wrapper div,” it’s often a sign your selector strategy is drifting.
Safer fixes than “make it more specific”
When something loses, the temptation is to add an id, chain more classes, or slap on !important. Try these approaches first.
- Fix load order: make sure your override stylesheet is loaded last.
- Prefer small, predictable selectors: a single class on the component root is easier to manage than deep descendant chains.
- Use CSS layers (if available): @layer lets you control cascade order intentionally instead of relying on file order.
- Remove the conflicting rule if it’s dead or overly broad (often the best long-term win).
- Scope “global” styles so they don’t accidentally style components (e.g., target a layout container instead of all h2 everywhere).
In other words: try to reduce conflicts, not just “win” them.
A quick “who wins?” mini-test you can do in your head
Pick the two selectors fighting, then ask:
- Does either one use !important?
- Does either one include an #id?
- If not, which has more class/attribute/pseudo-class parts?
- If tied, which rule appears later (or loads later)?
This takes about 10 seconds with practice and saves a lot of “why is this red again?” time.
Takeaway: aim for predictable CSS, not heroic overrides
Specificity isn’t a mystery once you treat it like a checklist: importance, selector strength, then order.
If you find yourself escalating specificity repeatedly, pause and fix the underlying conflict (load order, overly-broad rules, or unclear scoping) so the next change is easier, not harder.