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

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:

  1. Origin and importance (lowest → highest priority):
    • User-agent (browser defaults)
    • User stylesheet
    • Author stylesheet (your CSS)
    • CSS animations (active @keyframes declarations)
    • Author !important
    • User !important
    • User-agent !important
    • CSS transitions (active transitioning properties — highest)
  2. Specificity (within the same origin).
  3. 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 !important wars (each side escalating).
  • Hides real specificity problems.
  • Hard to override later without !important of 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:

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 — 1px1/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, not accent).
  • Update at runtime via JS: el.style.setProperty('--accent', '...').

See css-responsive-and-modern for dark mode / theming patterns.

9. References