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.#fffshort form expands to#ffffff.#1d4ed8aaadds an alpha channel. - RGB / RGBA:
rgb(29 78 216)orrgb(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.
/* 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); }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.
/* 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.
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:
.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:
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: swapso 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.
@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's1emscales with the text around it.clamp(min, preferred, max)- fluid typography. The browser picks a value, but never belowminor abovemax.
/* 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.
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.
h1, h2, h3 { text-wrap: balance; }
p { text-wrap: pretty; }Try it
Quick quiz
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.currentColorfor 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-heightunitless. Body 1.4-1.6, headings 1.1-1.25. Cap reading width around60ch.text-wrap: balanceon headings,text-wrap: prettyon body. Tiny wins, huge polish.