Colors, Units and Value Functions
Color notation
Four ways to write one color
.swatches {
color: #38bdf8; /* hex - compact, the most common in codebases */
color: rgb(56 189 248); /* rgb - same thing, decimal channels */
color: hsl(198 93% 60%); /* hsl - hue in degrees, saturation, lightness in % */
color: oklch(0.75 0.14 232); /* oklch - perceptually even, made for design systems */
}
/* Modern syntax: spaces, optional alpha after a slash - both are 60% opaque */
.veil {
color: rgb(56 189 248 / 0.6);
color: #38bdf899;
}
Why HSL and OKLCH earn their keep
Hex and RGB describe how a screen mixes light; HSL and OKLCH describe how humans see. For derived colors — a hover state, a muted variant, a full palette — you want to adjust lightness without hue drifting. HSL does this acceptably; OKLCH does it measurably better because its lightness axis matches perceived brightness. Plain CSS computes variants directly, no preprocessor required:
:root {
--brand: oklch(0.62 0.19 260);
--brand-hover: oklch(0.55 0.19 260); /* same hue, actually darker */
--brand-soft: oklch(0.62 0.19 260 / 0.15); /* same color, transparent */
}
/* Mixing two colors with a proportion - replaces a whole palette file */
.badge-soft {
background: color-mix(in oklch, var(--brand) 15%, white);
}
Absolute and relative units
The unit you pick is a design decision
| Unit | Relative to | Best for |
|---|---|---|
px | Nothing (device-independent pixel) | Borders, hairlines, shadows |
rem | Root <html> font size | Spacing and typography that must scale globally |
em | The element's own font size | Component-relative sizing (icon next to text) |
% | Parent box dimension | Fluid widths and heights |
ch | Width of the "0" glyph | Line-length limits (max-width: 70ch) |
vw / vh | Viewport width / height | Full-bleed sections; use with care on mobile |
dvh, svh, lvh | Dynamic / small / large viewport height | Mobile-safe full-height layouts |
rem and the user's font size
rem is anchored to the root font size — which the user controls in their browser settings.
Sizing typography and spacing in rem means a user who sets a 20px base gets a proportionally
larger interface, not a broken one. This is an accessibility feature, and it is why
html { font-size: 16px; } is an anti-pattern: it stamps out the user's choice.
/* Anti-pattern: overwrites the user's preferred font size */
html { font-size: 16px; }
/* Correct: leave the root alone, scale everything relative to it */
body { font-size: 1rem; } /* = whatever the user chose */
h1 { font-size: 2rem; } /* exactly twice the user's choice */
Value functions: the math you actually need
calc(), min(), and max()
/* calc() mixes units - impossible with any single unit */
.sidebar { width: calc(18rem + 2vw); }
/* min() and max() are a media query you can put on one property */
.page {
width: min(92%, 72rem); /* fluid, but never wider than 72rem */
padding-inline: max(1.5rem, 4vw); /* at least 1.5rem, more on big screens */
}
clamp() is three functions in one
clamp(min, preferred, max) equals max(min, min(preferred, max)). The preferred
value is usually a mix of rem (respects user settings) and vw (tracks the
screen). The result is fluid type with hard floors and ceilings — the standard technique for headings:
h1 {
/* never below 1.5rem, never above 2.75rem, fluid in between.
The rem part keeps it tied to the user's font size. */
font-size: clamp(1.5rem, 1rem + 2.5vw, 2.75rem);
}
Where functions fit with custom properties
:root {
--measure: 70ch; /* readable line length */
--gutter: clamp(1rem, 3vw, 2.5rem);
}
.prose { max-width: var(--measure); }
.layout { gap: var(--gutter); } /* one token, every breakpoint */
Custom properties are introduced fully in the Custom Properties lesson; here, note only that they store computed values, so a function result can be defined once and reused.
Practice: build a fluid token set
The task
Open demo/foundations/07_units_and_functions.html and
demo/foundations/08_color_spaces.html from the
Lab Examples page. Resize the preview and watch which
values move and which hold still.
The checklist
- Write a container width that is fluid but never exceeds
64rem— without a media query. - Convert a fixed
font-size: 24pxheading toremand verify it at a 20px user base. - Produce hover, soft, and border variants of one brand color using only
oklch()andcolor-mix(). - Explain to yourself why
dvhexists by opening any100vhpage on a phone with a URL bar.