CSS: selectors, cascade, specificity
Skip this page if you already know…
- how to read a specificity like
(0, 1, 0)and say who wins; - that source order breaks ties between rules of equal specificity;
- why
!importantfixes today and charges tomorrow; - what inherits and what does not.
The problem
You want the button red. You write this and nothing changes:
.button {
background: red;
}
So you write this, and it works:
.button {
background: red !important;
}
The bug was not fixed — it was postponed. Somewhere there is a rule that beat
yours, and now two rules are fighting, one of them holding a weapon that can only
be answered with another !important. Three months later the app has forty of them
and switching themes is impossible.
It is worth knowing who won.
A complete example
Single file, open it in a browser:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Cascade</title>
<style>
button {
background: gray;
}
.button {
background: blue;
}
#save {
background: green;
}
.bar .button {
background: orange;
}
</style>
</head>
<body>
<div class="bar">
<button id="save" class="button">Save</button>
</div>
</body>
</html>
The button comes out green. Four rules match it, and the #id one wins.
The arithmetic
Every rule has a three-number specificity — (id, class, type):
| Selector | id | class | type | Vector |
|---|---|---|---|---|
button |
0 | 0 | 1 | (0, 0, 1) |
.button |
0 | 1 | 0 | (0, 1, 0) |
.bar .button |
0 | 2 | 0 | (0, 2, 0) |
#save |
1 | 0 | 0 | (1, 0, 0) |
You compare left to right, and it is lexicographic, not a sum: (1, 0, 0)
beats (0, 99, 0). No number of classes ever reaches one id.
Counting as class: .class, [attribute], :hover, :focus, :not(...)
(the :not itself is worth zero, but what is inside counts). Counting as type:
div, button, ::before. * is worth zero.
Source order breaks ties — and only breaks ties
Two rules with the same specificity: the later one wins. That is how your app's CSS overrides a library's — as long as you import yours afterwards, at the same specificity.
What beats everything
The full order the browser applies, strongest first:
- User
!important(the user's own stylesheet — rare) - Author
!important(yours, and the library's) - Inline
style="..." - Normal author styles, resolved by layer → specificity → source order
!important is not "more specificity": it skips the whole queue. That is why
the only answer to it is another !important, and why it is debt.
The symptom that specificity is out of control
You write a selector longer than it needs to be (.page .card .title h2) just
to beat another one. Every time that happens, the next person needs an even
longer one. The way out is not to escalate — it is to lower both sides.
What inherits
Some properties pass from parent to child without you asking: color,
font-family, font-size, line-height, text-align, visibility — and
custom properties (--my-colour), which is what makes theming work.
These do not inherit: background, border, padding, margin, display,
width. They apply only to the element you wrote them on.
body {
color: #333; /* inherits: every text below becomes #333 */
border: 1px solid red; /* does not inherit: only body gets a border */
}
Where it shows up in the SDK
tempest-react-sdk styles only through CSS Modules, and every class comes out
of the build with a transformed name:
.button { } → .tempest_button_a3f9k { }
That is a specificity decision, not an aesthetic one. Every SDK rule is a single
class — (0, 1, 0) — because:
- It cannot collide. The hash guarantees the SDK's
.buttonand your app's.buttonare different selectors. Neither needs a long selector to defend itself. - It is cheap to override. Your own class, at the same
(0, 1, 0), imported aftertempest-react-sdk/styles.css, wins on source order. You never need!importantto adjust a component.
import "tempest-react-sdk/styles.css";
import "./app.css"; // later: your overrides win the tie-break
Overriding a token beats overriding a rule
Most of the time you do not need a class at all: changing --tempest-primary
on :root repaints the whole component without touching a single selector.
That is the subject of Custom properties and theming.
Why there is no headless mode
A recurring question is whether the SDK could emit data-* attributes instead
of classes, so apps could style with Tailwind or Stitches. The answer is no,
and the reason lives in Styles & Design Tokens: keeping a second styling
path would double every component's surface and dilute the tokens. A Tailwind
app coexists with the SDK — the tempest_ prefix guarantees nothing
collides.
Recap
- Specificity is the
(id, class, type)vector, compared left to right.(1,0,0)beats(0,99,0). ✅ - Source order breaks ties between equal specificities — that is how an app's CSS overrides a library's.
!importantis not high specificity, it is queue-jumping. It can only be answered with another, and that is how an app loses the ability to change themes.colorand custom properties inherit;background,borderandpaddingdo not.- In the SDK every rule is a single hashed
tempest_*class: it cannot collide and it is cheap to override — import your CSS afterstyles.css.
📚 Canonical reference: MDN — Cascade and inheritance
➡️ Next page: Box, flow, flex and grid