Typography

Chapter 3 — Typography, Color, and Backgrounds

Typography and colour are where design meets CSS. Most developers know the basic properties, but the details — font loading performance, colour space maths, gradient syntax edge cases, layered backgrounds — separate clean production work from fragile one-offs. This chapter is a complete reference for all three systems.

1. Typography

The font stack

A font stack is a comma-separated list of font families. The browser tries each in order, using the first one it finds installed. The last entry should always be a CSS generic family as a final fallback.

System UI (modern)
system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif
Native OS font on every platform. Zero load time. Used by GitHub, Linear, Notion.
Geometric sans
'Inter', 'Helvetica Neue', Arial, sans-serif
Load Inter from Google Fonts or self-host. Fallback to system Helvetica/Arial.
Readable serif
Georgia, 'Times New Roman', serif
Georgia is universally available. Good for long-form reading.
Code / monospace
'JetBrains Mono', 'Fira Code', 'Courier New', monospace
Load a web font, fall back to universally available Courier New.

Loading web fonts — @font-face

/* Self-hosting a web font — maximum performance control */ @font-face { font-family: 'Inter'; src: url('/fonts/inter-var.woff2') format('woff2'); font-weight: 100 900; /* variable font range */ font-style: normal; font-display: swap; /* show fallback immediately, swap when loaded */ } /* font-display values: */ /* auto — browser decides (usually block) */ /* block — invisible text for ~3s, then swap */ /* swap — fallback immediately, swap when ready (FOUT) */ /* fallback — 100ms invisible, then fallback, swap within 3s */ /* optional — 100ms invisible, then fallback, no swap if slow */
Use font-display: swap for body text, optional for decorative fonts. swap avoids invisible text (FOIT) at the cost of a layout shift (FOUT) when the font loads. optional avoids both — if the font doesn't load within 100ms, the browser uses the fallback permanently for that page load. Good for non-critical decorative fonts.

Variable fonts

/* Variable fonts expose axes — weight, width, slant, optical size */ h1 { font-family: 'Inter'; font-weight: 650; /* any value between 100–900 in a variable font */ font-variation-settings: 'wght' 650, 'opsz' 32; /* font-variation-settings overrides standard properties — use as last resort */ } /* Animating variable fonts */ @keyframes weight-pulse { from { font-variation-settings: 'wght' 300; } to { font-variation-settings: 'wght' 800; } }

All typography properties

PropertyKey valuesNotes
font-size px · em · rem · % · clamp() Use rem for consistent sizing. clamp() for fluid type. Avoid px on body — it overrides user browser settings.
font-weight 100–900 · bold · normal Numeric values work with variable fonts. bold = 700, normal = 400. The browser synthesises bold if the weight isn't available — looks terrible.
font-style normal · italic · oblique <angle> oblique with an angle only works on variable fonts with a slant axis.
line-height unitless · px · em · % Prefer unitless (e.g. 1.6) — it's relative to the element's own font-size and inherits cleanly. 1.4–1.7 for body, 1.1–1.3 for headings.
letter-spacing em · px · normal Use em so spacing scales with font-size. Positive = wider, negative = tighter. Common: 0.01em–0.05em for body, 0.1em+ for uppercase labels.
word-spacing em · px · normal Rarely needed — use sparingly and only with em units.
text-align left · right · center · justify · start · end Prefer start/end over left/right — they respect writing direction (RTL support). justify on screen often creates awkward rivers of whitespace.
text-transform uppercase · lowercase · capitalize · none capitalize uppercases the first letter of each word. Use uppercase + letter-spacing for labels/badges.
text-decoration none · underline · line-through · overline Shorthand for text-decoration-line, -color, -style, -thickness. Can set underline color independently of text color — great for styled links.
text-underline-offset px · em Moves underline away from baseline. 3–4px feels more intentional than the default browser position.
text-indent px · em · % Indents first line. Negative values with matching left padding create hanging indents.
white-space normal · nowrap · pre · pre-wrap · pre-line nowrap prevents line breaks. pre preserves whitespace + line breaks. pre-wrap wraps at container edge while preserving spaces.
overflow-wrap normal · break-word · anywhere break-word breaks long words to prevent overflow. anywhere is more aggressive. Use on containers with user-generated content.
text-overflow clip · ellipsis Only works when overflow: hidden and white-space: nowrap are also set.
font-variant small-caps · numeric · ligatures font-variant is a shorthand. font-variant-numeric controls number rendering (lining, oldstyle, tabular). font-variant-ligatures controls fi/fl ligatures.

Fluid typography with clamp()

/* clamp(minimum, preferred, maximum) */ h1 { font-size: clamp(1.75rem, 4vw + 1rem, 3.5rem); /* Never smaller than 1.75rem, never larger than 3.5rem */ /* Scales smoothly between viewport widths */ } /* Line length control — the most important typography rule */ .prose { max-width: 65ch; /* ch = width of the "0" character — ~60-75ch is ideal */ }

Single-line truncation vs multi-line clamping

/* Single-line ellipsis — requires all three */ .truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Multi-line clamp — limit to N lines */ .clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; /* Still the correct approach despite the -webkit- prefix — fully supported */ }

2. Color

Color systems — a complete picture

Hex
#4f8ef7 · #4f8ef7ff
Most common. 6-digit RGB hex, optional 2-digit alpha. 3-digit shorthand (#abc = #aabbcc). Opaque by default.
rgb() / rgba()
rgb(79 142 247) · rgb(79 142 247 / 0.8)
Modern syntax uses spaces, not commas. Alpha with / separator. rgba() is legacy — just use rgb() with 4 values.
hsl()
hsl(220 90% 64%) · hsl(220 90% 64% / 0.8)
Hue (0–360 degrees), Saturation %, Lightness %. Human-readable — easy to create colour palettes by adjusting lightness.
oklch() — modern
oklch(65% 0.18 260) · oklch(65% 0.18 260 / 0.8)
Perceptually uniform — equal lightness steps look equally different. Better for accessible colour palettes and gradients. Wide gamut capable (P3 and beyond).
color() — wide gamut
color(display-p3 0.3 0.56 0.97)
Access wide gamut colour spaces (display-p3, rec2020). Vivid colours outside sRGB. Falls back to sRGB on unsupported displays.
color-mix()
color-mix(in oklch, blue 60%, red)
Mix two colours in any colour space. oklch gives the most visually pleasing result. Great for generating tints and shades programmatically.
Why oklch beats hsl for design systems. In hsl, colours at the same declared lightness (e.g. yellow at 50% and blue at 50%) look wildly different in perceived brightness — yellow looks far lighter. oklch corrects for this: equal lightness values produce colours that actually look equally light. When building a design token system, defining your palette in oklch means accessible contrast ratios are predictable.

currentColor — the CSS colour inheritance keyword

/* currentColor equals the element's computed color value */ .icon { color: #58a6ff; border: 2px solid currentColor; /* same blue */ box-shadow: 0 0 8px currentColor; /* same blue */ } /* In SVGs: fill="currentColor" makes the SVG adopt its CSS color */ /* Essential for icon systems — one SVG, any colour via CSS */

Transparency and the alpha channel

/* Multiple ways to express transparency */ .el { color: rgb(0 0 0 / 0.5); /* 50% transparent black */ color: hsl(0 0% 0% / 50%); /* same */ color: #00000080; /* hex with alpha (80 hex = ~50%) */ color: transparent; /* fully transparent (= rgb(0 0 0 / 0)) */ /* opacity vs alpha — critical difference: */ opacity: 0.5; /* affects the element AND all its children */ background: rgb(0 0 0 / 0.5); /* affects only this property — children unaffected */ }

3. Backgrounds

All background properties

PropertyValuesNotes
background-color any colour value Rendered below background-image. Useful as a fallback when the image fails to load.
background-image url() · linear-gradient() · radial-gradient() · conic-gradient() · none Multiple images layer on top of each other (first listed = top layer). Gradients are treated as images.
background-size auto · cover · contain · px/% pairs cover fills the box (may crop). contain fits entire image (may leave gaps). Use cover for hero images, contain for logos.
background-position center · top left · 50% 25% · px values Sets where the image is anchored. Percentage values are relative to both container and image size (not just container).
background-repeat repeat · no-repeat · repeat-x · repeat-y · space · round space distributes copies evenly. round scales images to fit whole copies. Always use no-repeat with background-size: cover.
background-attachment scroll · fixed · local fixed = parallax effect (relative to viewport). local scrolls with content inside the element. scroll = default.
background-origin padding-box · border-box · content-box Where the background image starts. Default is padding-box (starts at the inner edge of the border).
background-clip border-box · padding-box · content-box · text text clips the background to the text shape — used for gradient text effects.
background shorthand Order: color image position/size repeat attachment origin clip. Resets all sub-properties — use longhand when layering multiple backgrounds.

Gradients

linear-gradient
linear-gradient(135deg, #58a6ff, #bc8cff)
radial-gradient
radial-gradient(circle at 30% 40%, #3fb950, #0d1117)
conic-gradient
conic-gradient(from 0deg, red, yellow, green, blue, red)
hard stops
same position twice = hard edge
/* Gradient syntax in full */ /* linear-gradient — direction then colour stops */ .box { background: linear-gradient(to right, #58a6ff, transparent); background: linear-gradient(135deg, #58a6ff 0%, #bc8cff 50%, #3fb950 100%); background: linear-gradient(in oklch, hsl(220 80% 60%), hsl(280 80% 60%)); /* "in oklch" interpolates colours through oklch space — avoids muddy midpoints */ } /* radial-gradient — shape, size, position */ .hero { background: radial-gradient(ellipse 80% 60% at center top, #1a2a4a, #0d1117); } /* conic-gradient — pie charts, angle wheels, starburst patterns */ .pie { background: conic-gradient(from 0deg, #58a6ff 0% 40%, #3fb950 40% 70%, #d29922 70% 100%); border-radius: 50%; } /* Hard colour stop — same position twice */ .stripe { background: linear-gradient(90deg, #58a6ff 0% 33%, #bc8cff 33% 66%, #3fb950 66% 100% ); }

Layered backgrounds

/* Multiple background layers — first listed renders on top */ .card { background-image: url('/icons/pattern.svg'), /* top: SVG pattern */ linear-gradient(135deg, #1a2a3a, #0d1117); /* bottom: gradient base */ background-size: 40px 40px, cover; background-repeat: repeat, no-repeat; background-position: center, center; }

Gradient text

/* Text with gradient fill — three properties required */ .gradient-text { background: linear-gradient(90deg, #58a6ff, #bc8cff); -webkit-background-clip: text; background-clip: text; color: transparent; /* make text transparent to show bg */ }

mix-blend-mode and background-blend-mode

/* mix-blend-mode: how the element blends with what's behind it */ .overlay { mix-blend-mode: multiply; /* darkens — good for image colour overlays */ mix-blend-mode: screen; /* lightens */ mix-blend-mode: overlay; /* contrast boost */ mix-blend-mode: color; /* applies hue+saturation, keeps luminosity */ } /* background-blend-mode: how an element's own bg layers blend together */ .duotone { background-image: linear-gradient(#58a6ff, #bc8cff), url('/photo.jpg'); background-blend-mode: multiply; /* duotone photo effect */ }

Chapter Summary

ConceptKey point
Font stacksAlways end with a generic family. System-ui is the fastest — no loading needed. Use font-display: swap to avoid invisible text during web font load.
Variable fontsOne file for all weights/widths. Use font-weight with numeric values (100–900). Avoid font-variation-settings except for non-standard axes.
line-heightUse unitless values (1.5–1.7 for body). They inherit correctly and scale with any font-size change. Never use px for line-height.
clamp()clamp(min, preferred, max) gives fluid type without breakpoints. 65ch max-width for prose line length.
Color systemshex/rgb for compatibility, hsl for readability, oklch for perceptual accuracy and design systems. color-mix() for programmatic tints/shades.
opacity vs alphaopacity affects the whole element tree. Alpha in a colour value (rgb / hsl / oklch with /) affects only that one property — children unaffected.
currentColorEquals the element's computed color. Use in SVG fill, border, box-shadow to keep them in sync with one property change.
background-sizecover fills and may crop. contain fits whole image and may leave gaps. Always use no-repeat with cover.
Gradientslinear, radial, conic. Hard stops = same position twice. Use "in oklch" to avoid muddy midpoints. Layer gradients for complex backgrounds.
Gradient textbackground-clip: text + color: transparent. Requires both -webkit- and unprefixed background-clip.
Exercises
  1. Font loading: Add a Google Font to a page using a <link> tag, then convert it to a self-hosted @font-face with font-display: swap. Compare load behaviour using DevTools Network tab (throttle to Slow 3G).
  2. Fluid type scale: Create a typographic scale — h1 through h3 and body — using clamp() for each, so sizes scale smoothly from mobile (320px) to desktop (1200px) with no breakpoints.
  3. Colour palette in oklch: Define a 5-step tint scale for one hue using oklch() — keep hue and chroma constant, vary lightness from 20% to 80% in equal steps. Compare the same steps in hsl(). Which looks more perceptually even?
  4. Hero section: Build a hero with a layered background: a semi-transparent gradient overlay on top of a photo, with a repeating SVG dot pattern at 10% opacity on top of both. All three layers in one background-image declaration.
  5. Gradient text heading: Create a heading that displays a three-colour gradient across the text using background-clip: text. Then animate the gradient position using @keyframes and background-position.
Next: Chapter 4 — Deep Dive: Selectors. Every CSS selector type in one place — combinators, attribute selectors, all pseudo-classes (structural, state, functional), pseudo-elements, and the modern relational selectors :is(), :where(), :not(), and :has().