CSS Selectors and the Cascade
- Description: How CSS rules target elements (selectors, specificity), how conflicts resolve (cascade, inheritance,
!important), and the box model. - My Notion Note ID: K2A-F2-1
- Created: 2018-03-23
- Updated: 2026-05-17
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Overview
- 2. Syntax and Ways to Apply CSS
- 3. Selectors
- 4. The Cascade
- 5. Specificity
- 6. Inheritance
- 7. The Box Model
- 8. Units and Values
- 9. References
1. Overview
CSS = Cascading Style Sheets — declarative rules that map HTML elements to visual presentation. Separates content (HTML) from appearance (CSS).
- Browser parses CSS → builds the CSSOM → merges with DOM → computes styles per element → paints.
- "Cascading" = many sources contribute styles for one element; conflicts resolve by origin → specificity → source order.
- Current spec: split into many CSS modules (Selectors 4, Color 5, Grid 2, etc.), each with its own version. No single "CSS5" — modules ship independently.
2. Syntax and Ways to Apply CSS
/* Rule = selector + declaration block */
selector {
property: value; /* declaration */
property: value;
}
/* Example */
h1.title {
font-size: 24px;
color: #333;
}
Three places to put CSS:
<!-- 1. External stylesheet (best) -->
<link rel="stylesheet" href="/styles.css">
<!-- 2. Internal <style> in <head> -->
<style>
h1 { color: red; }
</style>
<!-- 3. Inline style (avoid) -->
<h1 style="color: red;">Title</h1>
External wins on caching, reuse, and separation of concerns. Inline is the highest-specificity origin — hard to override; use only for JS-driven dynamic styles.
CSS comments: /* ... */ only. No // line comments — that's Sass/Less, not CSS.
3. Selectors
3.1 Basic Selectors
| Selector | Matches |
|---|---|
* |
Every element (universal). |
p |
All <p> (type/element). |
.note |
Elements with class="... note ...". |
#main |
The element with id="main". IDs should be unique. |
[type="email"] |
Elements with attribute type equal to "email". |
[href] |
Elements that have the attribute at all. |
[href^="https"] |
href starts with https. |
[href$=".pdf"] |
Ends with .pdf. |
[href*="example"] |
Contains example. |
3.2 Combinators
| Combinator | Meaning | Example |
|---|---|---|
A B (space) |
Descendant — B anywhere inside A. |
nav a |
A > B |
Direct child only. | ul > li |
A + B |
Adjacent sibling — B immediately after A. |
h2 + p |
A ~ B |
General sibling — any B after A with same parent. |
h2 ~ p |
3.3 Grouping
Comma-separates selectors that share a declaration:
h1, h2, h3 { font-family: serif; }
3.4 Pseudo-classes
State or position. Single colon :.
| Pseudo-class | Matches |
|---|---|
:hover |
Pointer over element. |
:focus |
Element has keyboard focus. |
:focus-visible |
Focus from keyboard (not mouse click). Prefer for focus rings. |
:focus-within |
Element or any descendant has focus. |
:active |
Element being activated (mouse/keyboard press). |
:visited |
Visited link (limited property support for privacy). |
:disabled, :enabled, :checked, :required, :valid, :invalid |
Form states. |
:first-child, :last-child, :only-child |
Position among siblings. |
:nth-child(2), :nth-child(odd), :nth-child(3n+1) |
Indexed siblings. |
:nth-of-type(...) |
Like :nth-child, but counts only siblings of the same tag. |
:not(.foo) |
Negation. Can take a selector list. |
:is(h1, h2, h3) |
Match any in list. Takes the highest specificity of its arguments. |
:where(...) |
Like :is, but contributes zero to specificity. Useful for opt-in defaults. |
:has(> img) |
Parent selector — element that contains a matching descendant. |
:empty |
No children, not even whitespace text. |
:root |
Document root (<html>). Conventional place for CSS variables. |
:has() is the long-awaited parent selector — landed in stable browsers in 2023.
3.5 Pseudo-elements
Style a synthetic part of the element. Double colon ::.
| Pseudo-element | Targets |
|---|---|
::before, ::after |
Generated content (requires content:). Common for icons, decorations. |
::first-line, ::first-letter |
First line / first letter of a block. |
::placeholder |
<input> placeholder text. |
::selection |
Highlighted text. |
::marker |
List item bullet/number. |
::backdrop |
Backdrop behind <dialog> or fullscreen element. |
.tooltip::after {
content: "?";
position: absolute;
/* ... */
}
4. The Cascade
When several rules target the same property on the same element, the cascade picks one. Order of resolution:
- Origin and importance (lowest → highest priority):
- User-agent (browser defaults)
- User stylesheet
- Author stylesheet (your CSS)
- CSS animations (active
@keyframesdeclarations) - Author
!important - User
!important - User-agent
!important - CSS transitions (active transitioning properties — highest)
- Specificity (within the same origin).
- Source order — last matching rule wins (within same specificity).
!important — escape hatch that bumps a declaration to a higher origin tier:
.foo { color: red !important; }
Use rarely. Reasons it's a code smell:
- Triggers
!importantwars (each side escalating). - Hides real specificity problems.
- Hard to override later without
!importantof your own.
Legitimate uses: utility classes (Tailwind uses !important for !-prefixed utilities), user stylesheets, overriding inline styles set by third-party scripts.
5. Specificity
Selector specificity is a tuple (a, b, c):
| Counter | Counts |
|---|---|
a |
IDs |
b |
Classes, attributes, pseudo-classes |
c |
Elements, pseudo-elements |
Universal *, combinators (>, +, ~), and :where() add zero.
Compare tuples lexicographically — (1,0,0) > (0,10,10).
/* a, b, c */
* /* 0, 0, 0 */
li /* 0, 0, 1 */
li::first-line /* 0, 0, 2 */
ul li /* 0, 0, 2 */
.foo /* 0, 1, 0 */
a[href]:hover /* 0, 2, 1 */
#main /* 1, 0, 0 */
ul#nav li.active a /* 1, 2, 2 */
:is(h1, .title) /* 0, 1, 0 — max of list */
:where(h1, .title) /* 0, 0, 0 — always zero */
style="..." /* inline, beats any selector */
Pitfall: stacking up classes (.foo.bar.baz) is the standard way to raise specificity without resorting to IDs or !important.
Tools:
- DevTools → Styles pane → strikethrough on overridden declarations + specificity tooltip.
- Specificity calculator: https://specificity.keegan.st/
6. Inheritance
Some properties inherit from parent to child by default — typography (color, font-*, line-height, text-align, visibility), cursor, direction. Most others don't (layout, borders, backgrounds, dimensions).
Force inheritance with explicit keywords:
| Keyword | Effect |
|---|---|
inherit |
Take the parent's computed value. |
initial |
Reset to the property's spec-defined initial value. |
unset |
inherit if inheritable, otherwise initial. |
revert |
Roll back to the previous origin (typically user-agent default). |
revert-layer |
Roll back to previous cascade layer. |
7. The Box Model
Every rendered element is a rectangular box, layered outward:
+------------------------------ margin --------------------------+
| +--------------------------- border ------------------------+ |
| | +------------------------ padding ----------------------+ | |
| | | +-------------------- content ----------------------+ | | |
| | | | | | | |
| | | | width × height | | | |
| | | | | | | |
| | | +---------------------------------------------------+ | | |
| | +-------------------------------------------------------+ | |
| +-----------------------------------------------------------+ |
+----------------------------------------------------------------+
box-sizing — what width/height actually measure:
| Value | width measures |
|---|---|
content-box (default) |
Just the content. Padding + border add to total size. |
border-box |
Content + padding + border. Padding and border eat into width. |
Almost everyone resets to border-box because the math is intuitive:
*, *::before, *::after { box-sizing: border-box; }
Pitfalls:
- Vertical margins collapse in three cases: adjacent in-flow siblings, parent and its first/last in-flow child, and empty blocks with no separators. The bigger margin wins; they don't add. Parent-child collapse is broken by giving the parent
padding/border/inline content or a new block formatting context (overflow: hidden,display: flow-root). Sibling collapse simply doesn't happen inside flex/grid containers (items aren't in normal block flow). - Margins of empty blocks collapse with themselves.
- Replaced elements (
<img>,<video>,<iframe>) have intrinsic dimensions —display: block+max-width: 100%keeps them responsive without distortion.
Display types — covered in css-layout:
display |
Behaviour |
|---|---|
block |
Fills parent width, breaks line before and after. Default for <div>, <p>, <h1>. |
inline |
Flows with text, ignores width/height/vertical margin. Default for <span>, <a>. |
inline-block |
Inline placement + block sizing. |
none |
Removed from layout and accessibility tree. |
flex, grid |
Layout containers — see css-layout. |
contents |
Element disappears; children promoted to parent. Useful for unwrapping wrappers. |
8. Units and Values
8.1 Length Units
Absolute — fixed physical size:
| Unit | Equivalence |
|---|---|
px |
CSS pixel — 1px ≈ 1/96in regardless of device pixel density. |
pt, pc, cm, mm, in |
Print units. Rare on screen. |
Relative — scale with context:
| Unit | Relative to |
|---|---|
em |
Parent's (for font-size) or element's own font-size (for everything else). Compounds in nested elements. |
rem |
Root element's font-size (default 16px). Use for typography — predictable scaling. |
% |
Parent's same property (width, height) or context-specific. |
vw, vh |
1% of viewport width / height. |
vmin, vmax |
1% of the smaller / larger viewport dimension. |
svw, svh, lvw, lvh, dvw, dvh |
Small / large / dynamic viewport (account for mobile browser chrome). |
ch |
Width of "0" in current font. Handy for column widths. |
ex |
x-height of current font. |
lh, rlh |
Line-height of element / root. |
html { font-size: 16px; }
body { font-size: 1rem; } /* 16px */
h1 { font-size: 2rem; } /* 32px */
.lead { font-size: 1.25em; } /* relative to parent — compounds */
8.2 Colours
color: red; /* named */
color: #ff0000; color: #f00; /* hex */
color: rgb(255 0 0); /* modern syntax, space-separated */
color: rgb(255 0 0 / 50%); /* alpha after slash */
color: hsl(0 100% 50% / 0.5); /* hue/saturation/lightness */
color: oklch(70% 0.2 30); /* perceptually uniform — recommended */
color: color-mix(in oklch, red 60%, blue);
OKLCH is the modern recommendation — designed for perceptual uniformity, easier to design colour scales by hand. Browser support is solid since 2023.
8.3 Functions
| Function | Use |
|---|---|
calc() |
Mixed-unit arithmetic. width: calc(100% - 20px). |
min(), max() |
Pick smaller / larger. width: min(100%, 60ch). |
clamp(MIN, IDEAL, MAX) |
Bounded fluid value. Workhorse for responsive typography. |
var(--name, fallback) |
Read a custom property. |
attr(name) |
Read an HTML attribute. Limited to content: in stable browsers. |
url(...) |
Reference an image, font, or other resource. |
h1 { font-size: clamp(1.5rem, 5vw, 3rem); }
8.4 Custom Properties
:root {
--accent: oklch(70% 0.2 30);
--space-3: 1rem;
}
.button {
background: var(--accent);
padding: var(--space-3);
}
- Inherit by default.
- Set per-element to scope overrides (theme switching, component variants).
- Dash-prefix to distinguish from spec properties (
--accent, notaccent). - Update at runtime via JS:
el.style.setProperty('--accent', '...').
See css-responsive-and-modern for dark mode / theming patterns.
9. References
- MDN CSS reference — https://developer.mozilla.org/en-US/docs/Web/CSS
- CSS Selectors Level 4 — https://www.w3.org/TR/selectors-4/
- CSS Cascading and Inheritance — https://www.w3.org/TR/css-cascade-5/
- Specificity calculator — https://specificity.keegan.st/