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:
- Origin and importance — author
!importantbeats author normal, and user-agent styles lose to both. - Cascade layers —
@layerorder, which is before specificity. - Specificity — the count below.
- Source order — last one wins.
Layers being above specificity is the modern part and the reason they exist:
@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.
: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:
!importanton a custom property does not do what you expect. It wins the cascade for the variable’s value, but the declaration that usesvar()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.
.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
*, *::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:
/* 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
.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.
Related
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@layerthan 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-boxchange?” -widthbecomes the whole visible box rather than just the content, so padding and border no longer add to it. The defaultcontent-boxis 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,unsetorrevert?” -initialis the spec default, which is often not what the browser shows.unsetinherits if the property is inheritable and is otherwiseinitial.revertgoes back to the user-agent stylesheet, which is usually what you actually meant.