# Foundations of CSS
A solid grasp of the box model, selectors, units, and the cascade is the backbone of CSS. These notes focus on practical, modern usage: lowering specificity, using new units, managing the cascade with layers, and writing resilient layouts.
# Table of Contents
- Box Model & Normal Flow
- Selectors & Specificity
- Units & Values
- Cascade, Inheritance & Layers
- Combinators & Pseudo-classes
- Margin Collapsing & Box Sizing
- Custom Properties Deep Dive
- Performance & Containment
- Selectors: Complete Reference
- Container Queries
- Color Spaces & Mixing
- Stacking Contexts & Painting
- Media & Environment Queries
- Typography & Variable Fonts
- Transforms & Animation Details
- Gotchas & Best Practices
- Quick Cheatsheet
- Practice Tasks
- Review Questions
- Related Links
# Box Model & Normal Flow
The box model layers are content, padding, border, and margin. Elements participate in normal flow as block or inline boxes. Creating a new block formatting context (BFC) can isolate layout behaviors (e.g., contain floats, prevent margin collapse).
/* Create a BFC to contain floats and prevent margin collapsing */
.container {
display: flow-root; /* clean, semantic replacement for clearfix hacks */
padding: 1rem;
border: 1px solid #ccc;
}
.label {
display: inline; /* inline boxes size to their content */
background: #eef;
padding: 0 .25rem;
}
2
3
4
5
6
7
8
9
10
11
12
<div class="container">
<p>The outer margin doesn't collapse because flow-root creates a new BFC.</p>
<span class="label">Inline label</span> continues on the same line.
<span class="label">Another</span>
<span class="label">Inline</span>
<span class="label">Box</span>
<p style="float: right">A floated element stays contained.</p>
<div style="clear: both"></div>
<p>Content after the float remains inside the container.</p>
</div>
2
3
4
5
6
7
8
9
10
11
Common BFC triggers: display: flow-root, overflow not visible, float, position: absolute|fixed, display: inline-block|table-cell|table-caption, flex/grid items.
Tip: Prefer flow-root for new code; it avoids extra markup and keeps intent explicit.
# Image Prompts
- Box model layers: Cross-section diagram labeling margin, border, padding, content.
- Normal flow: Column of block boxes with inline lines flowing left-to-right.
# Selectors & Specificity
Selectors target elements by type, attributes, position, and state. Specificity determines which rules win.
/* Use :where() to cap specificity for utilities */
:where(.btn) {
padding: .5rem 1rem;
border-radius: .25rem;
}
/* Variants add specificity via class combination (still low and manageable) */
.btn.primary {
background: var(--primary, #06f);
color: #fff;
}
/* Attribute selector with case-insensitive flag */
button[type="submit" i] { text-transform: uppercase; }
2
3
4
5
6
7
8
9
10
11
12
13
14
<button class="btn primary">Primary Button</button>
<button class="btn">Default Button</button>
2
- Specificity is a 4-tuple: inline, IDs, classes/attributes/pseudo-classes, elements/pseudo-elements. Example:
#header .nav li.active→ (0,1,2,2). :where()always contributes zero specificity; great for utilities and resets.:is()contributes the specificity of its most specific argument; good for compact selectors.- Avoid IDs and inline styles in application CSS; prefer classes to keep specificity low and overrides predictable.
- Attribute selector flags:
i(case-insensitive),s(case-sensitive where applicable).
# Selectors: Complete Reference
This section catalogs CSS selectors by type with examples, specificity notes, and practical tips.
# Basic
- Universal
*: matches any element. Specificity (0,0,0,0). - Type
h1,p,input: element names. Specificity (0,0,0,1). - Class
.btn,.card: Specificity (0,0,1,0). - ID
#app: Specificity (0,1,0,0) — avoid in app code where possible.
# Attributes
- Presence:
[disabled] - Exact:
[type="email"] - Space-separated word:
[class~="token"] - Dash-separated prefix:
[lang|="en"](matchesen,en-US) - Prefix:
[href^="https://"] - Suffix:
[src$=".png"] - Substring:
[data-id*="user-"] - Flags: case-insensitive
i, case-sensitives. Specificity equals a class selector.
# Combinators
- Descendant:
A B - Child:
A > B - Adjacent sibling:
A + B - General sibling:
A ~ B - Column:
A || B(table columns and associated cells)
# Pseudo-classes — State/UI
- Links:
:any-link,:link,:visited - Focus:
:focus,:focus-visible,:focus-within - Forms/UI:
:enabled,:disabled,:read-only,:read-write,:required,:optional,:checked,:indeterminate,:placeholder-shown,:autofill,:valid,:invalid,:user-invalid - Targeting:
:target,:target-within - Dialog/popover:
:modal,:popover-open(support varies) - Language/direction:
:lang(en),:dir(rtl)
# Pseudo-classes — Structural
- Position:
:first-child,:last-child,:only-child,:nth-child(An+B),:nth-last-child(An+B) - Per-type:
:first-of-type,:last-of-type,:only-of-type,:nth-of-type(An+B),:nth-last-of-type(An+B) - Empty:
:empty(no children or text nodes) Tip: any whitespace/text node prevents:emptyfrom matching.
# Relational and Selector Lists
:is(S1, S2, ...)— matches if any argument matches; specificity equals the most specific argument.:where(S1, S2, ...)— same matching as:isbut zero specificity.:not(S)— negates; specificity equals its argument (level 4 allows complex lists).:has(R)— parent/ancestor matching based on relative selector R.:scope— reference for relative selectors (root in query context or@scope).
Examples:
ul > li:nth-child(2n of .item) { background: color-mix(in oklab, #06f 10%, white); }
form:has(input:invalid) { outline: 2px solid crimson; }
:where(.btn, .tag) { border-radius: .25rem; }
button:not(.primary) { opacity: .8; }
2
3
4
# Pseudo-elements
- Generated:
::before,::after - Text:
::first-line,::first-letter,::selection,::target-text - Lists/forms:
::marker,::placeholder,::file-selector-button - Overlays/media:
::backdrop,::cue,::cue-region - Diagnostics:
::spelling-error,::grammar-error - Shadow DOM:
::part(name),::slotted(selector); with hosts:host,:host(),:host-context()Specificity: like an element selector.
# Specificity Notes
- Classes, attributes, and pseudo-classes share the same weight.
:is()and:not()take the highest specificity of their arguments;:where()stays at zero.- IDs trump most patterns; reserve for one-off overrides or anchors.
- Many chained classes increase specificity; consider layer ordering and utilities to keep it low.
# Native CSS Nesting
Use & as the nest selector; combine with relational selectors.
.card {
padding: 1rem;
& > h2 { margin: 0 0 .5rem; }
& .actions { display: flex; gap: .5rem; }
&:has(img) { grid-template-columns: 1fr min(40%, 20rem); }
}
2
3
4
5
6
Notes: & refers to the parent selector; nesting does not change specificity rules.
# Units & Values
Modern CSS has robust units. Choose based on what the value should scale with:
- Font-relative:
em,rem,ex,ch,cap,ic,lh,rlh. - Viewport:
vw,vh,vmin,vmaxand stable/dynamic variantssvw/svh,lvw/lvh,dvw/dvh. - Container query units:
cqw,cqh,cqi,cqb,cqmin,cqmax(size relative to the nearest query container). - Layout: grid
frunit (flexible track sizing for CSS Grid only). - Percentages: resolve against different references depending on property (e.g., width vs. transform).
- Sizing keywords:
min-content,max-content,fit-content(). - Functions:
calc(),min(),max(),clamp()for safe fluid ranges.
:root {
--space-1: clamp(0.5rem, 1vw, 0.75rem);
--space-2: clamp(1rem, 2vw, 1.5rem);
}
.card {
width: min(48ch, 90cqi); /* cap width by characters or container inline size */
padding: var(--space-2);
margin: var(--space-2) auto;
border-radius: .5rem;
background: #fff;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(22ch, 30cqi), 1fr));
gap: var(--space-1);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="card">
<p>This card scales smoothly with viewport or container size.</p>
<p>Try resizing the container to see cqi at work.</p>
</div>
<div class="grid">
<div>A</div><div>B</div><div>C</div>
<div>D</div><div>E</div><div>F</div>
<div>G</div>
</div>
2
3
4
5
6
7
8
9
Notes:
- Use
dvh/dvwto account for dynamic browser UI on mobile; prefer overvh/vwwhen full-viewport sizing matters. frworks only in Grid. For Flexbox, use percentages orflexvalues instead.
# Cascade, Inheritance & Layers
The cascade decides which declarations apply. Inheritance passes some properties from parent to child (e.g., color, font-family). Cascade layers (@layer) add an explicit order of precedence across groups of rules.
Cascade order (high → low), conceptually:
- Origin & importance (user-important > author-important > author > user > UA)
- Layer order (later-declared layers win over earlier ones; unlayered rules sit on top of all layers)
- Specificity
- Source order
@layer reset, base, components, utilities;
@layer reset {
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
}
@layer base {
:root { color-scheme: light dark; }
body { font: 1rem/1.5 system-ui, sans-serif; }
}
@layer components {
.card { background: #fff; border-radius: .5rem; padding: 1rem; }
}
@layer utilities {
.mt-2 { margin-top: .5rem; }
.text-center { text-align: center; }
}
/* Unlayered rules override all layers by default */
.card { box-shadow: 0 1px 4px rgb(0 0 0 / 0.1); }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="card mt-2 text-center">
Layered styling order is explicit with @layer declarations.
Unlayered overrides still apply last.
</div>
2
3
4
5
Useful keywords: inherit, initial, unset (behaves like inherit for inherited properties, else initial), revert (revert to user/UA default), revert-layer (revert within the current layer stack).
WARNING
When adding layers to an existing project, declare the full layer list in one place before use to avoid accidental reordering across files. Keep overrides either in the final layer or unlayered.
# Combinators & Pseudo-classes
Combinators express relationships: descendant (), child (>), adjacent sibling (+), general sibling (~). Pseudo-classes target stateful conditions or relationships.
/* Adjacent siblings: style only paragraphs immediately following an h2 */
h2 + p {
margin-top: 0;
font-style: italic;
}
/* :has() is supported in modern browsers; select parents with matching descendants */
form:has(input:invalid) {
border-left: 4px solid var(--danger, crimson);
padding-left: 1rem;
}
/* Accessibility: focus styles only when keyboard focus is visible */
button:focus-visible { outline: 2px solid color-mix(in oklab, #06f, white 20%); }
/* Grouped interactive states without raising specificity too much */
:is(a, button)[aria-disabled="true"], :is(a, button).disabled {
pointer-events: none;
opacity: .6;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<form>
<label>Email <input type="email" required></label>
<button class="btn">Submit</button>
</form>
2
3
4
Note: :has() enables powerful parent/ancestor styling; still use thoughtfully for performance on very large DOMs.
# Margin Collapsing & Box Sizing
Adjacent vertical margins between block-level boxes in normal flow may collapse (top with bottom). Collapsing can happen between a parent and its first/last child. It does not occur for flex/grid items or inside a BFC.
box-sizing controls whether padding/border are included in the declared width/height (prefer border-box for predictable layout).
*, *::before, *::after { box-sizing: border-box; }
.pane {
width: 50%;
padding: 1rem;
border: 2px solid #000;
}
.wrapper {
display: flow-root; /* prevents child margins collapsing with parent */
}
2
3
4
5
6
7
8
9
10
11
<div class="wrapper">
<div class="pane" style="margin-top: 2rem;">Top margin doesn't collapse with parent.</div>
<div class="pane" style="margin-top: 1rem;">Another pane.</div>
</div>
2
3
4
# Custom Properties Deep Dive
CSS custom properties participate fully in the cascade and compute-time value resolution. They inherit by default and can store any token sequence.
/* Define tokens with fallbacks */
:root {
--brand: oklch(60% 0.16 260);
--gap: 1rem;
}
.card { gap: var(--gap, 1rem); border-color: var(--brand, rebeccapurple); }
/* Typed registration enables transitions and validation */
@property --spin {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.loader { rotate: var(--spin); transition: --spin 400ms linear; }
.loader.is-active { --spin: 360deg; }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Notes:
- Resolution happens at computed-value time;
var()fails the whole declaration if the property becomes invalid after substitution, unless a fallback is provided. - Registered properties via
@propertygain type safety and are animatable. - Custom properties can be queried in container style queries (see Container Queries) enabling design tokens that drive variants.
# Performance & Containment
Use rendering containment to limit layout, paint, and style recalculations for large pages and lists.
/* Prevent off-screen content from being laid out/painted until needed */
.card-list > li {
content-visibility: auto;
contain-intrinsic-size: 1px 200px; /* reserve space to avoid CLS */
}
/* Isolate heavy components */
.widget {
contain: content; /* size + layout + paint + style containment */
}
/* Isolate stacking/painting to avoid unexpected blending */
.overlay {
isolation: isolate;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Tips:
content-visibility: autodramatically improves first render for long pages; always pair withcontain-intrinsic-sizefor stable layout.- Containment can change behavior (e.g., margin-collapsing, z-index). Validate visuals.
# Advanced Selectors
For a full catalog and specificity notes, see Selectors: Complete Reference.
Level 4 selectors unlock powerful patterns.
/* :nth-child with of-list filters */
li:nth-child(2n of .item) { background: color-mix(in oklab, #06f 10%, white); }
/* :not() carries the specificity of its argument; keep it low */
button:not(.primary) { opacity: .8; }
/* :dir() matches document direction; combine with logical props */
[dir="rtl"] .nav:dir(rtl) { margin-inline-start: 0; }
2
3
4
5
6
7
8
Notes:
:nth-child(An+B of selector)filters the counting set; great for striping only certain rows.:not(S)takes S’s specificity (but never adds more than one simple selector’s worth).- Prefer
:lang()/:dir()over attribute checks when semantics matter.
# Container Queries
Size and style your components relative to their container instead of the viewport.
.card-grid { container: grid / inline-size; }
/* Size query */
@container (width > 40rem) {
.card { grid-template-columns: 1fr 1fr; }
}
/* Named containers */
.sidebar { container-name: sidebar; container-type: inline-size; }
@container sidebar (min-width: 22rem) { .nav { display: grid; } }
/* Style query (custom properties only) */
.theme-scope { --variant: filled; container-type: inline-size; }
@container style(--variant: filled) {
.button { background: var(--brand); color: white; }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Notes:
- Use
container-type: inline-sizeon a parent to enable queries; addcontainer-nameto target specific containers. - Style queries currently match custom properties on the query container; use them to drive component variants.
# Color Spaces & Mixing
Work in perceptual color spaces for smoother scales and blends.
:root {
/* Perceptual spaces */
--primary: oklch(62% 0.14 265);
--primary-strong: color-mix(in oklab, var(--primary), black 10%);
}
.btn { background: var(--primary); color: white; }
.btn:hover { background: var(--primary-strong); }
2
3
4
5
6
7
8
Notes:
- Prefer
lab/lchoroklab/oklchfor generating ramps and mixing; they produce more even lightness steps than sRGB. color-mix()interpolates in a specified color space; useoklab/oklchfor UI scales.- Use
color-scheme: light darkand system colors to align with user preferences.
# Stacking Contexts & Painting
Understanding stacking contexts prevents z-index bugs.
Creates a new stacking context:
- Positioned element (
positionnotstatic) withz-index. - Any element with
opacity < 1,transform,filter,mix-blend-mode,perspective,isolation: isolate,will-change: transform, orcontain: paint.
Paint order basics:
- Background/borders
- Descendants with negative
z-index - In-flow/inline descendants
- Positioned descendants with
z-index: auto/0 - Positioned descendants with positive
z-index
Debug tip: Temporarily add outline: 1px solid red and transform: translateZ(0) carefully to reveal stacking/containment issues.
# Media & Environment Queries
Modern queries adapt to capabilities and user preferences.
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; }
}
@media (color-gamut: p3) {
:root { --primary: color(display-p3 0.2 0.4 1); }
}
/* Respect safe areas on notched devices */
.hero { padding-top: max(2rem, env(safe-area-inset-top)); }
2
3
4
5
6
7
8
9
10
Useful features: prefers-contrast, prefers-color-scheme, forced-colors, light-level, pointer, hover, dynamic-range.
# Typography & Variable Fonts
Control legibility and rhythm with advanced font features.
/* Variable font with optical sizing */
@font-face {
font-family: "InterVar";
src: url(/fonts/Inter-Variable.woff2) format("woff2-variations");
font-display: swap;
unicode-range: U+000-5FF; /* latin */
}
body {
font-family: InterVar, system-ui, sans-serif;
font-optical-sizing: auto; /* use 'opsz' axis if present */
font-variant-numeric: tabular-nums lining-nums; /* better alignment */
}
.price { font-feature-settings: "ss01" 1; /* named stylistic set */ }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Notes:
- Prefer high-level properties (
font-variant-*) overfont-feature-settingswhere possible. font-size-adjusthelps equalize perceived size across fallback fonts.- Use
line-heightunit types likelh/rlhfor consistent rhythm.
# Transforms & Animation Details
Modern transform properties and math unlock smooth, GPU-friendly animations.
/* Individual transform properties */
.tile { translate: 0 0; rotate: 0deg; scale: 1; transition: translate .3s, rotate .3s, scale .3s; }
.tile:hover { translate: 0 -4px; scale: 1.02; }
/* Trigonometric functions for periodic motion */
.wave { --t: 0deg; translate: 0 calc(5px * sin(var(--t))); }
/* Prefer opacity/transform for composited animations */
.fade-in { opacity: 0; animation: fade 300ms ease-out forwards; }
@keyframes fade { to { opacity: 1 } }
/* Avoid layout thrash during animation */
.will-move { will-change: transform; }
2
3
4
5
6
7
8
9
10
11
12
13
Notes:
translate/rotate/scaleare animatable individually and often perform better than thetransformshorthand.- Trig functions (
sin,cos,tan,atan2) supportdeg/radinputs; combine withcalc()for expressive motion. - Use
prefers-reduced-motionto provide alternatives for motion-sensitive users.
# Gotchas & Best Practices
- Keep specificity low: prefer classes; avoid IDs and inline styles.
- Use logical properties (
margin-inline-start,padding-block, etc.) for internationalization and writing modes. - Prefer
flow-rootover clearfix hacks to contain floats/margins. - Avoid fixed heights; let content define height, or cap with
max-height. - Use
dvh/dvwfor full-viewport sections on mobile; avoid jumps from dynamic UI. - Introduce layers early:
@layer reset, base, components, utilities, overridesis a pragmatic default. - Use
:where()to scope utility helpers at zero specificity. - Favor custom properties with fallbacks:
var(--gap, 1rem); centralize tokens on:root. - Reach for container queries and
cqw/cqhwhen component sizing should respond to container, not the viewport. - Consider
@scopefor local scoping of styles to a subtree (emerging support). Combine with layers to avoid bleed. - Shadow DOM styling: use
:host,:host-context,::part,::slottedto bridge component boundaries safely.
# Quick Cheatsheet
- Box model: content + padding + border + margin.
- Specificity: inline > ID > class/attr/pseudo-class > element/pseudo-element.
- Units:
px,%,rem/em,ch,lh,vw/vh/vmin/vmax,svw/svh,lvw/lvh,dvw/dvh, gridfr, containercqw/cqh/.... - Functions:
min(),max(),clamp(),calc(); size keywordsmin-content,max-content,fit-content(). - Cascade: origin/importance → layers → specificity → source order. Unlayered > layered.
# Practice Tasks
- Create a small spacing utility set using
:where()(e.g.,.gap-1–.gap-4) and verify that component classes can override them via source order. - Build a responsive card that uses
clamp()for font-size andmin()for width; comparevhvs.dvhon mobile. - Introduce
@layer base, components, utilities, overridesto a sample page and demonstraterevert-layerand an unlayered override. - Use
:has()to style a<form>when any required input is invalid, and add:focus-visiblestyles for accessibility. - Create a grid using
frtracks and explain whyfrdoesn’t apply to Flexbox.
# Review Questions
- Why does
:where()have zero specificity, and how does that differ from:is()? - Give an example selector that yields specificity
(0,1,0,2)and explain the count. - Why is
box-sizing: border-boxrecommended for layout? - How do
min()andclamp()enable fluid yet bounded design? - Describe a margin-collapsing scenario and two ways to prevent it.
- What is the relative order of layers versus specificity in the cascade, and where do unlayered rules sit?
- When should you favor container units (
cqw/cqh) over viewport units?
# Related Links
- /css/02-layout-essentials.md
- /css/03-styling-techniques.md
- /css/05-variables-architecture.md