Colors, Units & Typography
px vs em vs rem vs %, every color format, font-family stacks, and web fonts — the values you will type in every single stylesheet.
Two Families of CSS Units — and Why the Distinction Matters
Every length value in CSS — a width, a font size, a margin — is expressed in a unit, and CSS units split into two fundamentally different families. Absolute units always represent the same physical size, regardless of anything else on the page. Relative units are computed based on some other value — the parent's font size, the viewport dimensions, or the root element's font size — which means the same declared number can render at different actual sizes depending on context.
.box {
width: 300px;
font-size: 16px;
}
/* 300px is always 300px, everywhere it's used, regardless of the
parent element, the user's browser settings, or anything else. */px (pixels) is the only absolute unit used in everyday CSS — the others defined by the specification (cm, in, pt, and similar) are intended for print stylesheets and essentially never appear in web layout code. Even px is not perfectly "absolute" in the physical sense on every device (high-density displays render CSS pixels using multiple physical device pixels), but for CSS's purposes it behaves as a fixed, predictable unit that does not scale with anything else on the page.
%, em, rem, vw, and vh — each relative to a different reference, covered one at a time in the next two parts.Percentage and Viewport Units
% is relative to the corresponding dimension of the element's containing block — most commonly its parent. A width of 50% means "half of my parent's width," and because it recomputes automatically whenever the parent's size changes, it is one of the oldest tools for building layouts that adapt to their container.
.parent {
width: 800px;
}
.child {
width: 50%; /* computes to 400px — half of the PARENT's width */
}
/* If .parent's width ever changes, .child's rendered width updates
automatically — no CSS change required. */vw and vh (viewport width/height) are relative to the browser window's viewport instead of any parent element — 1vw is exactly 1% of the viewport's width, and 1vh is exactly 1% of its height, regardless of what element they are applied to or how deeply nested it is.
.hero {
height: 100vh; /* always exactly the full viewport height */
width: 100vw; /* always exactly the full viewport width */
}
.hero-title {
font-size: 5vw; /* text that scales continuously with window width —
common for large, responsive hero headings */
}50vw as half the entire browser window, completely ignoring the size of any of its ancestors. Reach for percentage units when you want sizing relative to a parent container, and viewport units specifically when you want sizing relative to the actual screen/window, regardless of nesting.em vs rem — and Why rem Wins for Font Sizing
em and rem are both relative to a font size — but relative to different font sizes, and that difference is exactly what makes one of them far more predictable to use.
.parent {
font-size: 20px;
}
.child {
font-size: 1.5em; /* 1.5 × the PARENT's font-size = 30px */
padding: 1em; /* 1 × the CHILD's OWN font-size (now 30px) = 30px padding */
}Notice the two different references in that single example — for font-size itself, em looks at the parent's font size; for every other property (like padding above), em looks at the element's own, already-computed font size. This dual behavior is exactly what makes em hard to reason about in nested structures — the effective size compounds at every nesting level that also sets a font size in em.
.list {
font-size: 1.2em; /* 1.2 × parent's size */
}
.list .list { /* a nested list, inside another .list */
font-size: 1.2em; /* 1.2 × the ALREADY-scaled parent — compounds! */
}
/* Three levels of nested lists, each at 1.2em, does not give you
1.2x the base size three times independently — it multiplies:
1.2 × 1.2 × 1.2 = 1.728x the original base size, growing faster
than most engineers expect just from reading the CSS. */rem ("root em") fixes this entirely by always referencing the root element's (<html>) font size — never the immediate parent, no matter how deeply nested the element is. The default root font size in every browser is 16px unless a user or a stylesheet changes it.
html {
font-size: 16px; /* the default, shown explicitly here */
}
.deeply .nested .element {
font-size: 1.5rem; /* ALWAYS 1.5 × 16px = 24px, no matter how
deeply nested this selector is, and no
matter what font-size any ancestor sets */
}px font sizes ignores that user preference completely; one built on rem scales with it automatically.Where em is still genuinely the right choice
em is not obsolete — it is the right tool specifically when you want a value to scale with the element's own font size, such as padding or spacing inside a button whose text size might vary by size variant (small/medium/large), so the padding automatically stays proportional without a separate override for each size.
.btn {
font-size: 16px;
padding: 0.75em 1.5em; /* scales automatically if font-size changes */
}
.btn--large {
font-size: 20px;
/* padding recalculates automatically: 0.75em is now 15px, not 12px —
no separate padding override needed for the larger variant */
}Every Way to Write a Color in CSS
CSS supports several distinct ways to express the same color, and real stylesheets mix them depending on the situation — mostly hex for fixed brand colors, and rgb()/hsl() when transparency or programmatic color manipulation is involved.
.box {
color: #f97316; /* 6-digit hex: RR GG BB, each pair 00-ff */
background: #333; /* 3-digit shorthand — each digit doubles: #333 = #333333 */
}.box {
color: rgb(249, 115, 22); /* same orange as #f97316 */
background: rgba(0, 0, 0, 0.5); /* black at 50% opacity */
}
/* Modern CSS also allows the space-separated syntax with a slash
for alpha, which behaves identically: */
.box {
background: rgb(0 0 0 / 50%);
}.box {
color: hsl(24, 95%, 53%); /* same orange again */
background: hsla(24, 95%, 53%, 0.3); /* the same orange, 30% opacity */
}
/* hsl() is often easier to reason about for programmatic color changes:
keep hue and saturation fixed, and just adjust lightness to get a
family of related shades — exactly how CSS-in-JS theme systems and
design-token tools commonly generate hover/active state colors. */.box {
color: tomato;
background: rebeccapurple;
border-color: transparent;
}
/* There are 147 named colors in the CSS spec. They are convenient for
quick prototyping, but real design systems almost always use hex or
hsl() instead, tied to actual brand color values. */8-digit hex — the lesser-known alpha variant
Hex colors also support an 8-digit form (and a 4-digit shorthand) that adds an alpha channel, exactly like rgba() — two extra hex digits (00 to ff) at the end for opacity.
.overlay {
background: #000000cc; /* black at roughly 80% opacity (cc = 204/255) */
}
/* Equivalent to: rgba(0, 0, 0, 0.8) */currentColor is a special CSS keyword worth knowing — it always resolves to the element's own computed color value, which makes it useful for things like a border or an SVG fill that should always match whatever text color is currently active, including through hover states, without duplicating the color value in two places.font-family — Always a Fallback List, Never a Single Font
font-family accepts a comma-separated list, not a single value, and the browser walks the list left to right, using the first font it actually has available on the user's system. This is called a font stack, and writing one is standard practice — a website that specifies only a single, specific font risks that font simply not being installed on a visitor's device, silently falling back to the browser's default (often an unstyled serif font).
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
/* The browser tries 'Helvetica Neue' first. If it isn't installed,
it tries plain Helvetica. If that also isn't available, Arial.
sans-serif at the very end is a GENERIC family, not a specific
font — a final guaranteed fallback every browser can resolve. */Font names containing a space (like Helvetica Neue) must be quoted; single-word names do not strictly require quotes but are commonly quoted anyway for consistency.
The five generic font families
Every font stack should end with one of CSS's five generic families — a guaranteed, always-available fallback that tells the browser roughly what kind of font to substitute if nothing else in the list matched.
sans-serif /* clean, no decorative strokes — most UI text */
serif /* decorative strokes at letter ends — traditional body text */
monospace /* fixed-width — code blocks, terminal output */
cursive /* handwriting-style */
fantasy /* decorative, rarely used */body {
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
/* system-ui resolves to San Francisco on macOS/iOS, Segoe UI on
Windows, Roboto on Android — no font file download required at all. */Loading Custom Fonts — @font-face in Brief
When a design needs a specific font that is not guaranteed to be installed on visitors' devices, the font file itself has to be shipped to the browser — either self-hosted or loaded from a service like Google Fonts. @font-face is the CSS rule that registers a custom font, giving it a name your font-family declarations can then reference like any other font.
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
body {
font-family: 'Inter', system-ui, sans-serif;
}font-display: swap is worth calling out specifically — it tells the browser to render text immediately using a fallback font while the custom font file is still downloading, then swap it in once it arrives, rather than leaving text invisible until the font finishes loading (the default behavior, which can produce a jarring flash of invisible text on a slow connection).
<!-- in the HTML <head> -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet">Hosted font services generate the @font-face rules automatically and serve the font files from their own CDN — trading a small amount of control for convenience and, often, better caching (many sites already share the same Google Fonts URL, so a visitor may already have the file cached from a completely different website).
font-display: swap.font-weight and line-height — The Two Properties Behind Every Readable Page
font-weight controls how bold text renders. It accepts numeric values from 100 (thinnest) to 900 (boldest) in steps of 100, plus the keywords normal (equivalent to 400) and bold (equivalent to 700).
p {
font-weight: 400; /* normal — same as font-weight: normal */
}
h1 {
font-weight: 700; /* bold — same as font-weight: bold */
}
.thin-heading {
font-weight: 300; /* light — only renders if the font FILE actually
includes a light weight variant */
}font-weight: 300 when only the regular (400) and bold (700) weights of a font were loaded typically causes the browser to either fall back to the nearest available weight, or in some cases synthetically "fake" the weight by stretching the regular glyph outlines — which usually looks noticeably worse than a real light-weight font file. When using a custom web font, load the specific weights the design actually needs.line-height — vertical spacing between lines of text
line-height controls the vertical space allotted to each line of text, and it is one of the highest-impact properties for readability — text set too tight is hard to read across multiple lines, and text set too loose feels disconnected.
p {
line-height: 1.6; /* UNITLESS — 1.6 × this element's OWN font-size.
This is the recommended form. */
}
p {
line-height: 24px; /* a FIXED pixel value — does not scale if
font-size changes on this element or a
descendant that inherits it */
}
p {
line-height: 150%; /* computed ONCE relative to this element's
font-size, then INHERITED as that fixed
computed value — a subtle trap covered next */
}The unitless form is strongly preferred, and the reason is inheritance: a unitless line-height is inherited as the raw ratio, so each descendant recalculates it against its own font size. A percentage or pixel value, by contrast, is computed once on the element that declares it and then inherited as that fixed, already-computed value — meaning a nested element with a larger font-size than its parent can end up with visually cramped lines, because it inherited a pixel value calculated for a smaller font size.
.parent {
font-size: 16px;
line-height: 150%; /* computes to 24px, and 24px is what's INHERITED */
}
.parent .child {
font-size: 32px; /* larger text... */
/* ...but still inherits a FIXED 24px line-height from the parent —
way too tight for 32px text, and visually cramped. */
}
/* Using line-height: 1.5 (unitless) on .parent instead would let
.child compute its OWN line-height as 1.5 × 32px = 48px — correctly
proportional to its own, larger font size. */An Accessibility Audit at a Chicago EdTech Startup
A Chicago-based EdTech startup building a reading platform for K-12 students commissions a third-party accessibility audit before a school-district contract renewal. The report flags one issue affecting nearly every page on the site: increasing the browser's text-size setting (a standard low-vision accommodation, and one many school districts explicitly test for) does almost nothing — body text barely grows at all.
What the auditor finds in the codebase
Every font-size declaration across the site's stylesheets is written in px, hardcoded against the assumption of a 16px root font size that never actually changes when a user adjusts their browser's minimum or default font size setting — because pixel values are, by definition, absolute and do not respond to that setting at all.
body {
font-size: 16px;
}
h1 { font-size: 32px; }
h2 { font-size: 24px; }
p { font-size: 16px; }
.caption { font-size: 12px; }The fix
The team converts every font-size declaration to rem, keeping the exact same visual proportions (since 16px, at the default 16px root size, is precisely 1rem), but now genuinely responsive to a user's font-size preference — because rem always recomputes relative to the root element's font size, which is exactly the value a browser's accessibility setting adjusts.
body {
font-size: 1rem; /* 16px at the default root size */
}
h1 { font-size: 2rem; } /* 32px, same as before */
h2 { font-size: 1.5rem; } /* 24px, same as before */
p { font-size: 1rem; } /* 16px, same as before */
.caption { font-size: 0.75rem; } /* 12px, same as before */
/* Visually IDENTICAL at default settings — but now every value scales
correctly when a user increases their browser's font size, since
rem always tracks the root element's font size. */The audit also flags line-height values set in fixed pixels for the same reason — switched to unitless values so line spacing scales proportionally alongside the now-responsive font sizes, exactly as covered in Part 07. This is a genuinely common finding in real accessibility audits: a site that looks correct by every visual measure can still fail a legally relevant accessibility requirement purely because of a unit choice made early in the project, long before anyone thought to test with an adjusted browser font size.
Four Misconceptions About Colors, Units, and Typography
5 Interview Questions — With Complete Answers
Colors, Units & Typography Mistakes Beginners Make Constantly
Rendering Problems You Will Hit — And Exactly Why
🎯 Key Takeaways
- ✓px is the only commonly used absolute unit; %, em, rem, vw, and vh are all relative to something else, which is exactly what makes them useful for responsive design.
- ✓% is relative to the containing block (usually the parent); vw/vh are relative to the browser viewport itself, regardless of nesting depth.
- ✓em is relative to the parent's font-size (for the font-size property) or the element's own font-size (for everything else), and compounds across nested elements set in em.
- ✓rem is always relative to the root <html> element's font-size — flat, predictable regardless of nesting, and the standard choice for font-sizing in production CSS.
- ✓CSS colors can be written as hex (#f97316), rgb()/rgba(), hsl()/hsla(), or named keywords — all four are equivalent ways to express the same color, with rgb()/hsl() and their 8-digit hex equivalent supporting an alpha (transparency) channel.
- ✓font-family is always a fallback list ending in a generic family (sans-serif, serif, monospace) — never a single font name alone.
- ✓@font-face registers a custom font file for use in font-family; always pair it with font-display: swap to avoid a flash of invisible text while it loads.
- ✓Prefer a unitless line-height (e.g. 1.6) over a pixel or percentage value — it is inherited as a ratio and recalculates correctly for descendants with a different font-size.
What comes next
Module 20 goes deep on selectors — combinators, pseudo-classes, pseudo-elements, and the exact numeric specificity calculation only introduced conceptually back in Module 17.
Module 20 → CSS Selectors Deep DiveDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.