webdev.complete
💄 CSS — The Skin
🧱Foundations
Lesson 12 of 117
25 min

Color & Typography

Modern color (oklch), variable fonts, fluid type with clamp().

Two things make a site feel professional: color and type. Two things make a site feel amateurish: same list, applied poorly. Modern CSS has tools that didn't exist five years ago - wider color spaces, variable fonts, fluid typography. Use them and your site stops looking like a Bootstrap demo from 2014.

Color formats, briefly

  • Named: red, cornflowerblue. Convenient for debugging, terrible for design systems.
  • Hex: #1d4ed8. Compact, universal. #fff short form expands to #ffffff. #1d4ed8aa adds an alpha channel.
  • RGB / RGBA: rgb(29 78 216) or rgb(29 78 216 / 0.6). Modern syntax uses spaces and a slash for alpha.
  • HSL / HSLA: hsl(220 80% 50%). Hue (0-360°), Saturation (%), Lightness (%). Easy to reason about "same color but lighter."
  • OKLCH: oklch(0.55 0.18 260). Lightness, Chroma, Hue. Perceptually uniform- equal numeric steps look like equal visual steps. This is the modern professional's choice.
css
/* All four are the same blue */
.a { color: #1d4ed8; }
.b { color: rgb(29 78 216); }
.c { color: hsl(220 80% 48%); }
.d { color: oklch(0.5 0.2 260); }
Why OKLCH is winning
In HSL, hsl(60 100% 50%) (yellow) looks much brighter than hsl(240 100% 50%) (blue) even though both claim 50% lightness. OKLCH fixes that. If you build a palette by stepping lightness 0.1 at a time, the steps actually look even. Designers care.

Modern helpers: color-mix, light-dark

color-mix() blends two colors. Useful for hover states, tints, and shades without maintaining a 12-step palette.

css
/* 80% brand color, 20% black - a slightly darker brand */
.button:hover {
  background: color-mix(in oklch, var(--brand), black 20%);
}

/* 60% brand, 40% white - a soft tint */
.card-header {
  background: color-mix(in oklch, var(--brand) 60%, white);
}

light-dark() is a recent addition that returns one color in light mode and another in dark mode, no media query needed.

css
html { color-scheme: light dark; }

body {
  background: light-dark(white, #0a0a0a);
  color: light-dark(#111, #eee);
}

currentColor: the chameleon

currentColoris a value that resolves to the element's current color. It's how you make an SVG icon automatically match the surrounding text color:

css
.icon {
  fill: currentColor;
  /* SVG fills match whatever color the parent text is */
}

button { color: white; }
button:hover { color: yellow; }
/* Both: the icon's fill follows along, no separate rule needed */

Fonts: the system stack

Web fonts are great, but they cost bytes and add a flash of unstyled text. The system font stackuses whatever native font the user's OS provides. Instant load, no extra requests:

css
body {
  font-family:
    system-ui,
    -apple-system,
    "Segoe UI",
    Roboto,
    "Helvetica Neue",
    Arial,
    sans-serif;
}

That stack gives you SF on Apple, Segoe on Windows, Roboto on Android. Each feels native because it is native. Use this for content sites where speed matters more than brand-specific type.

Web fonts and variable fonts

When you do want a custom font, load it with @font-face. Or use Google Fonts / Fontsource. Two performance tips that aren't optional:

  • Use font-display: swap so text renders in a fallback while the custom font loads.
  • Use a variable font when one is available. A single file covers every weight (100 to 900) and often width and slant too. Replaces 4-9 separate font files.
css
@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter.var.woff2") format("woff2-variations");
  font-weight: 100 900;  /* range, not a single value */
  font-display: swap;
}

h1 { font-weight: 800; }
p  { font-weight: 400; }
/* Same file. Different weights. */

Sizing: rem, em, clamp()

Three units cover 95% of typography:

  • rem - relative to the root font size (usually 16px). 1rem = 16px, 1.5rem = 24px. Use for most font sizes, padding, margins.
  • em- relative to the current element's font size. Useful inside components: an icon that's 1em scales with the text around it.
  • clamp(min, preferred, max) - fluid typography. The browser picks a value, but never below min or above max.
css
/* Fluid heading: scales from 1.5rem on small screens to 3rem on big ones */
h1 {
  font-size: clamp(1.5rem, 4vw + 1rem, 3rem);
}

/* Comfortable body text */
body { font-size: 1rem; line-height: 1.6; }

/* Constrain reading width */
p { max-width: 60ch; }

4vw + 1remmeans "4% of the viewport width plus 16px." That formula gives smooth growth across screen sizes. Theclamp wrapper prevents extremes.

Why px isn't evil but rem is better
Users can change the default font size in their browser. If you set everything in px, you ignore that preference. With rem, your whole scale grows when they bump up the root size. Respect the user.

Line height, letter spacing, and the small things

  • line-height: unitless (e.g. 1.5) is best - it scales with the font size. Aim for 1.4-1.6 on body text, 1.1-1.25 on headings.
  • letter-spacing: usually 0. Slightly negative on big headings (-0.02em) for a tighter feel.
  • font-feature-settings / font-variant-numeric: enable tabular numbers ("tnum") for tables, or ligatures, or stylistic sets.

text-wrap: balance and pretty

Recent CSS additions that fix two long-standing typography sins.

  • text-wrap: balance- evenly distribute text across lines. Perfect for headlines so you don't get one long line and one orphan word.
  • text-wrap: pretty - prevents orphans (single words on the last line of a paragraph). Use on body text.
css
h1, h2, h3 { text-wrap: balance; }
p          { text-wrap: pretty; }

Try it

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <article>
      <h1>The quick brown fox jumps over the lazy dog</h1>
      <p>
        Try changing the OKLCH lightness on .accent. Try the
        text-wrap rules with and without them. Resize the window
        to watch the heading scale fluidly.
      </p>
      <button class="btn">Hover me</button>
    </article>
  </body>
</html>

Quick quiz

Quiz1 / 4

Why prefer rem over px for font-size?

Recap

  • Color formats: hex, rgb, hsl, and the modern winner OKLCH (perceptually uniform).
  • color-mix() for tints/shades. light-dark() for theme-aware colors. currentColor for inheriting fills.
  • Start with the system font stack. Reach for web fonts when you need brand. Prefer variable fonts when available.
  • Size with rem for layout, em for component-relative, clamp() for fluid scaling.
  • line-height unitless. Body 1.4-1.6, headings 1.1-1.25. Cap reading width around 60ch.
  • text-wrap: balance on headings, text-wrap: pretty on body. Tiny wins, huge polish.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.