Frontend / CSS / 05_cascade_specificity_and_box_model.md

Cascade, specificity and the box model

Updated 6 interview angles 4 min read source
On this page7
  1. Which declaration wins
  2. Specificity is three numbers, not one
  3. Inheritance is separate from the cascade
  4. The box model, and the one line everyone writes
  5. Logical properties
  6. Related
  7. Interview angle

Cascade, specificity and the box model

The two things that decide what a page actually looks like, and the two things people work around instead of learning. Almost every “CSS is unpredictable” complaint is one of these.

Which declaration wins

The cascade resolves conflicts in this order, and it stops at the first step that separates them:

  1. Origin and importance — author !important beats author normal, and user-agent styles lose to both.
  2. Cascade layers@layer order, which is before specificity.
  3. Specificity — the count below.
  4. Source order — last one wins.

Layers being above specificity is the modern part and the reason they exist:

css
@layer reset, framework, app;      /* order declared once, up front */

@layer framework {
  #sidebar .nav a { color: blue; }   /* specificity 1-2-1 */
}
@layer app {
  a { color: red; }                  /* specificity 0-0-1 — and it WINS */
}

A later layer beats an earlier one regardless of specificity. That is what lets you override a framework without an !important arms race — see Modern Selectors and Cascade Layers.

Specificity is three numbers, not one

Count (ids, classes, elements); compare left to right. A single id beats any number of classes:

Selector Specificity
* 0-0-0
a 0-0-1
.nav 0-1-0
a.nav:hover 0-2-1
#main 1-0-0
#main .nav a 1-1-1
inline style="" overrides all of the above

Things that do not add specificity: :where() is always 0-0-0, and the combinators >, +, ~ count for nothing. :not() and :is() take the specificity of their most specific argument.

css
:where(.a, #b) p { }      /* 0-0-1 — the :where() part is free */
:is(.a, #b) p { }         /* 1-0-1 — #b sets it */

:where() is the tool for authoring low-specificity defaults that are trivial to override, which is what a component library wants.

Gotcha: !important on a custom property does not do what you expect. It wins the cascade for the variable’s value, but the declaration that uses var() is still an ordinary declaration and can be overridden normally.

Inheritance is separate from the cascade

Only some properties inherit — colour, font, line-height, visibility. Layout properties do not, which is why setting border on a parent does not give every child a border.

css
.child { color: inherit; }    /* force it */
.child { all: revert; }       /* back to the user-agent style */

The four global values are worth knowing: inherit, initial (the spec default, often not what the browser shows), unset (inherit if inheritable, else initial) and revert (back to the previous origin, usually the browser’s stylesheet). revert is almost always the one you actually want.

The box model, and the one line everyone writes

css
*, *::before, *::after { box-sizing: border-box; }

Default content-box means width is the content width, so padding and border are added on top — a width: 200px element with 20px padding occupies 240px. border-box makes width the whole visible box, which is how everyone thinks about it.

Margins collapse, which is the other surprise:

css
/* Adjacent siblings: 30px between them, not 50px. */
.a { margin-bottom: 30px; }
.b { margin-top: 20px; }

Vertical margins between siblings collapse to the larger of the two, and a parent’s margin collapses with its first or last child’s unless something separates them — padding, a border, or a new block formatting context. Only vertical, only in normal flow: flex and grid children never collapse, which is one quiet reason modern layout feels more predictable.

Logical properties

css
.card { margin-inline: auto; padding-block: 1rem; }

inline is the text direction, block is perpendicular to it. In English that is horizontal and vertical; in Arabic or Japanese it is not. Writing margin-inline instead of margin-left/margin-right makes a layout mirror correctly under dir="rtl" for free, and it is now the default idiom.

Interview angle 6

  • “How does the cascade decide?” - origin and importance, then cascade layers, then specificity, then source order — stopping at the first step that separates them. Layers sitting above specificity is the modern part, and it is what lets you override a framework without !important.
  • “How is specificity calculated?” - three counts: ids, classes (plus attributes and pseudo-classes), elements, compared left to right. One id beats any number of classes. Combinators add nothing, :where() is always zero, and :is()/:not() take their most specific argument.
  • “How do you override a library without !important?” - put it in an earlier @layer than yours; layer order beats specificity outright. Failing that, :where() to author your own defaults at zero specificity so they are easy to override in turn.
  • “What does box-sizing: border-box change?” - width becomes the whole visible box rather than just the content, so padding and border no longer add to it. The default content-box is why a 200px element with 20px padding occupies 240px.
  • “What is margin collapsing?” - adjacent vertical margins in normal flow merge into the larger one, and a parent collapses with its first or last child unless padding, a border or a new formatting context separates them. Flex and grid children never collapse.
  • initial, unset or revert?” - initial is the spec default, which is often not what the browser shows. unset inherits if the property is inheritable and is otherwise initial. revert goes back to the user-agent stylesheet, which is usually what you actually meant.