← back to the section

Half of the "mystery" of CSS is the same situation over and over: you wrote a rule, but the element looks different. The style isn't "broken" — it lost to another rule. The mechanism for picking the winner is called the cascade (the very C in CSS), and it is strictly deterministic. Work through it once — and the "mystery" ends.

The cascade: three questions in order

When several rules compete for the same property of an element, the browser compares them by three criteria — strictly in sequence, the next one is consulted only on a tie in the previous:

  1. Origin and importance. Author styles (yours) beat browser styles (user agent). !important flips the order within an origin.
  2. Selector specificity — the "weight" of the selector, covered below.
  3. Order in the code — on a complete tie, whatever is written later wins.

Two practical consequences follow immediately. First: the default look of a button or a link is also CSS (the browser's), and your rules override it on equal terms. Second: the order in which stylesheets are included matters — "later" only wins at equal specificity.

Specificity: counting a selector's weight

Specificity is conveniently pictured as a triple of numbers (A, B, C):

  • A — the number of #ids in the selector;
  • B — the number of classes (.btn), attributes ([type="text"]), and pseudo-classes (:hover);
  • C — the number of tags (button) and pseudo-elements (::before).

Comparison is digit-by-digit: first A, on a tie B, then C. A hundred classes will not outweigh one id — the digits do not "overflow".

button          /* (0,0,1) */
.btn            /* (0,1,0) — beats button */
.card .btn      /* (0,2,0) — beats .btn */
#submit         /* (1,0,0) — beats everything above */
button.btn:hover /* (0,2,1) */

The universal selector * and combinators (>, +, ~, space) carry no weight. An inline style style="..." is stronger than any selector; only !important can override it.

A typical battle in a real project looks like this: the component style .button (0,1,0) suddenly loses to an ancient rule #sidebar a (1,0,1) — and you're already writing #sidebar a.button, escalating the arms race. The right way out is different: keep specificity flat — one class per rule, no ids in selectors, no long chains. Then the cascade is driven by code order, which is predictable. That is exactly the strategy methodologies like BEM use and CSS Modules automate.

One paragraph is enough for !important: it does not "strengthen" a rule, it exits the competition into a separate league, where only another !important with higher specificity can beat it. The first !important in a project spawns a second — an avalanche. Legitimate cases: overriding a third-party inline style (a widget, an email) and utility classes that must always win.

Inheritance: what flows down on its own

Some properties an element receives from its parent without any rules: text properties — color, font-*, line-height, text-align — inherit; box properties — margin, padding, border, background, width — do not. The logic is simple: text inside a card should look like the card's text, but the card's border should not appear on every paragraph inside.

That's why base typography is set once on body and flows everywhere by itself:

body {
  font-family: Inter, sans-serif;
  color: #1a1a2e;
  line-height: 1.5;
}

Inheritance can be controlled explicitly: inherit (take from the parent — even for non-inherited properties), initial (the value per the specification), unset (inherit if inheritable, reset otherwise), revert (go back to the browser style).

CSS variables: inheritance put to work

Custom properties are values you declare yourself and reuse via var():

:root {
  --color-accent: #2f5b4f;
  --space: 8px;
}
.button {
  background: var(--color-accent);
  padding: var(--space) calc(var(--space) * 2);
}

Two things make them more powerful than preprocessor variables. They inherit like ordinary properties — you can override them on a subtree, and everything inside recalculates:

.dark-theme { --color-accent: #8fd3c4; } /* dark theme in a single rule */

And they live at runtime — they are visible from and mutable in JavaScript, they animate, they work in media queries. Theming, design tokens, per-component local settings — all of that is the cascade and inheritance working for you, not against you.

In short

  • A style conflict is resolved in sequence: origin/importance → specificity → order in the code. The next criterion applies only on a tie.
  • Specificity is a triple (ids, classes/attributes/pseudo-classes, tags). Digit-by-digit comparison: a hundred classes won't outweigh one id.
  • Keep specificity flat: one class per rule, no ids — then the cascade is driven by predictable code order.
  • !important is a separate league with no way back; for overriding third-party inline styles, not for everyday use.
  • Text properties inherit, box properties don't. Base typography — once on body.
  • CSS variables inherit and live at runtime: themes and tokens are their native job.
  • Selectors — what that very specificity is made of.
  • Styling in a React application — how CSS Modules and tokens solve the cascade problem organizationally.
  • The box model — the next foundation block: what an element's size is made of.