CSS Fundamentals — Cheat Sheet
HTML, CSS & Responsive · 6 topics. Download the PDF or the Instagram carousel and share it.
The Cascade, Specificity and Inheritance
When two rules apply, one wins. Knowing exactly how that is decided is the difference between debugging CSS and adding !important until it works.
- ✓The cascade resolves in order: origin and importance, then specificity, then source order
- ✓Specificity is compared column by column — one id beats any number of classes
- ✓:where() has zero specificity and @layer sets priority explicitly, both of which prevent override wars
- ✓Text properties inherit; box properties do not — and form controls need font: inherit explicitly
- ✓The DevTools Styles panel shows the winner at the top with losers struck through, which answers most CSS questions instantly
/* 1. ORIGIN and IMPORTANCE
author !important > author > user agent (browser defaults)
2. SPECIFICITY — counted as (inline, id, class, element)
3. SOURCE ORDER — the last matching rule wins */
#main .card p /* (0,1,1,1) */
.card p.intro /* (0,0,2,1) */
p /* (0,0,0,1) */
.btn /* (0,0,1,0) */
style="color:red" /* (1,0,0,0) — beats every selector */
/* Compare left to right. A single id beats any number of classes:
(0,1,0,0) > (0,0,20,0) — specificity does not "add up" across
columns, which surprises people. */
/* Pseudo-classes count as classes; pseudo-elements as elements */
a:hover /* (0,0,1,1) */
li::marker /* (0,0,0,2) */
/* These three add nothing at all */
:where(.a, .b) /* (0,0,0,0) — always zero */
* /* (0,0,0,0) */
:is(#x) /* takes the HIGHEST of its arguments — careful */The Box Model and Spacing
Every element is a box with content, padding, border and margin. Two rules — border-box sizing and single-direction margins — remove most sizing surprises.
- ✓box-sizing: border-box makes width include padding and border, which is what people expect — set it globally
- ✓Adjacent vertical margins collapse to the larger value, and a child's margin can escape its parent
- ✓Flex and grid gap never collapses, which is why gap-based spacing avoids the whole problem
- ✓Prefer constraints — max-width, min(), aspect-ratio — over fixed pixel sizes
- ✓overflow: hidden creates a formatting context and silently breaks position: sticky inside it
/* Default: content-box. width applies to the CONTENT only. */
.card { width: 300px; padding: 20px; border: 2px solid }
/* Rendered width = 300 + 40 + 4 = 344px. */
/* border-box: width includes padding and border. */
*, *::before, *::after { box-sizing: border-box }
.card { width: 300px; padding: 20px; border: 2px solid }
/* Rendered width = 300px, with 256px of content. */
/* This is why 'width: 50%' with padding overflows its parent under
the default and behaves under border-box. Set it once, globally,
and never think about it again. */
/* The four layers, outside in:
margin — space OUTSIDE, transparent, can collapse, not in width
border — the visible edge
padding — space INSIDE, takes the background
content — the text or child boxes */
/* Padding vs margin: background reaches into padding, not margin */
.button { padding: .5rem 1rem } /* clickable, coloured */
.button { margin: .5rem 1rem } /* space around, not clickable */Selectors and Pseudo-Classes
Beyond classes there is a whole vocabulary — combinators, structural pseudo-classes, :has() — that removes the need for extra markup and JavaScript.
- ✓Combinators and nth-child style position without adding classes; > * + * is the standard spacing idiom
- ✓Attribute selectors can style from ARIA state, keeping CSS and accessibility in sync
- ✓:focus-visible targets keyboard focus only, which is why it replaced the outline-removal habit
- ✓:has() selects a parent by its contents and removes a surprising amount of JavaScript
- ✓Generated content from ::before is decorative — it is not reliably announced, selectable or translatable
.card .title /* descendant, at any depth */
.card > .title /* direct child only */
.a + .b /* the next sibling, immediately after */
.a ~ .b /* any following sibling */
/* Structural — no extra classes needed */
li:first-child li:last-child li:only-child
li:nth-child(2) li:nth-child(odd) li:nth-child(3n)
li:nth-last-child(2)
p:first-of-type /* first p, regardless of other siblings */
:empty /* no children at all, not even text */
/* Two very useful idioms */
.stack > * + * { margin-block-start: 1rem } /* space between */
tr:nth-child(even) { background: var(--surface) } /* zebra stripes */
/* Attribute selectors */
a[href^="http"] /* starts with */
a[href$=".pdf"] /* ends with */
[data-state="open"] /* exact */
[aria-invalid="true"] /* style from ARIA state — keeps CSS and
accessibility in sync automatically */
/* :is() and :where() shorten long lists */
:is(h1, h2, h3) + p { margin-block-start: .5rem }Units — px, rem, em, %, and the Viewport
The unit you choose decides whether your layout respects a user who has increased their browser font size. rem for type and spacing, px for hairlines, and the viewport units that actually work on phones.
- ✓rem scales with the user's browser font setting, so type and spacing in px ignore an accessibility preference
- ✓The html { font-size: 62.5% } trick halves the text for anyone who changed their default — avoid it
- ✓em is relative to the current element and compounds through nesting
- ✓100vh on phones is the large viewport and hides content under the address bar; dvh follows the chrome
- ✓clamp() replaces sizing media queries, but mix a rem term into the preferred value so it still respects font settings
/* rem — relative to the ROOT font size, which the user controls.
1rem = 16px by default, and 24px if they changed it. */
h1 { font-size: 2rem } /* scales with the user's setting */
.card { padding: 1.5rem }
/* NEVER do this to make rem maths easier — it halves the text size
for everyone who changed their browser default: */
html { font-size: 62.5% } /* the "1rem = 10px" trick. Don't. */
/* em — relative to the CURRENT element's font size. Ideal for
things that must scale with their own text. */
.button { padding: .5em 1em } /* padding grows with the label */
.badge { font-size: .75em } /* three-quarters of its parent */
/* em compounds through nesting, which is the usual gotcha:
a .badge inside a .badge is .5625em. */
/* px — fixed. Correct for things that should NOT scale: */
border: 1px solid /* a hairline is a hairline */
box-shadow: 0 1px 2px
/* The practical rule:
font-size, margin, padding, gap, border-radius -> rem
borders, shadows, hairlines -> px
component-internal spacing that tracks its text -> em */Colour and Typography
Readable text is mostly four decisions — size, line height, line length and contrast — and web fonts are the most common cause of a slow, shifting page.
- ✓Body text at 1rem, line-height 1.5 unitless, and a measure of 45–75 characters cover most of readability
- ✓A line-height with units does not scale for children, which is why unitless is the rule
- ✓font-display: swap avoids invisible text, and a font preload needs crossorigin even same-origin
- ✓WCAG AA requires 4.5:1 for body text and 3:1 for large text and UI elements — it is a floor, not a preference
- ✓Never encode meaning in colour alone, and respect prefers-color-scheme and prefers-reduced-motion
body {
font-size: 1rem; /* 16px minimum for body text. Never 12px. */
line-height: 1.5; /* unitless, so it scales with font-size */
max-width: 65ch; /* 45-75 characters per line */
}
h1 { font-size: 2.5rem; line-height: 1.1 } /* tighter for large text */
/* line-height with a unit does NOT scale for children — this is why
unitless is the rule: */
body { line-height: 1.5 } /* correct */
body { line-height: 24px } /* a 32px heading also gets 24px */
/* A modular scale keeps sizes coherent rather than arbitrary */
--step-0: 1rem; --step-1: 1.25rem; --step-2: 1.563rem; --step-3: 1.953rem;
/* Other text properties worth knowing */
letter-spacing: .05em /* only for UPPERCASE labels */
text-wrap: balance /* even heading line lengths */
text-wrap: pretty /* no single-word last line in a paragraph */
hyphens: auto
font-variant-numeric: tabular-nums /* digits align in columns */
/* Do not justify text on the web — it creates rivers of whitespace
without the hyphenation engine print has. */Custom Properties and Theming
CSS variables are live values that inherit and cascade, which makes theming, dark mode and component variants a matter of redefining tokens rather than rewriting rules.
- ✓Custom properties are live cascade values, not compile-time substitutions — they inherit and can be redefined per subtree
- ✓Layer semantic tokens over raw primitives so a palette change touches one place
- ✓Theming means redefining tokens in three blocks — base, prefers-color-scheme, and an explicit data-theme
- ✓Defining a colour only inside a dark-mode media query leaves it undefined in light mode
- ✓They can be read and written from JavaScript, and @property gives them a type so they can animate
:root {
/* primitives — the raw palette */
--blue-600: #2563eb;
--grey-900: #111827;
/* semantic tokens — what the UI actually references */
--color-accent: var(--blue-600);
--color-text: var(--grey-900);
--color-surface: #ffffff;
--space-1: .25rem; --space-2: .5rem; --space-4: 1rem;
--radius: .5rem;
--shadow-sm: 0 1px 2px rgb(0 0 0 / .06);
}
.button { background: var(--color-accent); border-radius: var(--radius) }
/* Two layers matters: components reference MEANING, so swapping the
palette changes one line rather than forty. */
/* Fallbacks for a possibly-undefined variable */
color: var(--color-text, #111)
padding: var(--card-padding, var(--space-4, 1rem))
/* They inherit, so a subtree can override */
.card { --color-accent: var(--green-600) } /* only inside this card */