Skip to content
academia.sh

Lesson 26 / 26

CSS Functions

Mixing units with calc, setting bounds with min and max, clamping a range with clamp, and combining these functions to produce a scale.

Contents

Units are defined, but individual fixed values still get written one by one. The style sheet has dimensions calculated by hand and written in: max-width: 70ch, padding-inline: 1rem, font-size: 2.4414rem. Some of these values depend on another, and the link does not show up in the file.

This lesson defines the functions that carry the calculation into the style sheet itself. It is the course’s last lesson; at the end, the style sheet built throughout the course gets wrapped up.

Four Functions

calc() — calculates a length with the four operations. Different units can get mixed; resolution happens at the stage where the values are known.

min() and max() — pick the smallest or largest of comma-separated values. They can take more than two values.

clamp() — takes three values: minimum, preferred, maximum. It clamps the preferred value between the two bounds.

// functions.mjs — resolving calc, min, max, and clamp values
const px = (v) => +v.toFixed(2);

// clamp(lo, pref, hi) = max(lo, min(pref, hi))
const clamp = (lo, pref, hi) => Math.max(lo, Math.min(pref, hi));

console.log("--- clamp: font size by viewport width ---");
console.log("font-size: clamp(1rem, 0.75rem + 1.2vw, 1.5rem)   (root 16px)");
for (const vw of [320, 480, 640, 800, 1024, 1440]) {
  const preferred = 0.75 * 16 + 1.2 * vw / 100;
  const result = clamp(1 * 16, preferred, 1.5 * 16);
  const which = result === 16 ? "lower bound" : result === 24 ? "upper bound" : "preferred";
  console.log(`viewport ${String(vw).padStart(4)}px -> preferred ${px(preferred)}px, applied ${px(result)}px  (${which})`);
}

console.log("\n--- calc: mixing different units ---");
const CONTAINING = 640, ROOT = 16;
const expressions = [
  ["100% - 2rem",      CONTAINING - 2 * ROOT],
  ["100% / 3",         CONTAINING / 3],
  ["100% / 3 - 16px",  CONTAINING / 3 - 16],
  ["50% + 20px",       CONTAINING * 0.5 + 20],
  ["2rem * 1.5",       2 * ROOT * 1.5],
];
for (const [expr, result] of expressions) {
  console.log(`calc(${expr})`.padEnd(24) + `-> ${px(result)}px   (containing ${CONTAINING}px, root ${ROOT}px)`);
}

console.log("\n--- min and max: which one sets the bound ---");
for (const containing of [320, 640, 1200]) {
  const minResult = Math.min(containing * 0.9, 600);
  const maxResult = Math.max(containing * 0.9, 600);
  console.log(`containing ${String(containing).padStart(4)}px -> min(90%, 600px)=${px(minResult)}  max(90%, 600px)=${px(maxResult)}`);
}

console.log("\n--- clamp's equivalence with min/max ---");
for (const pref of [10, 16, 20, 24, 30]) {
  const a = clamp(16, pref, 24);
  const b = Math.max(16, Math.min(pref, 24));
  console.log(`preferred=${String(pref).padStart(2)} -> clamp=${a}  max(16,min(pref,24))=${b}  equal=${a === b}`);
}

console.log("\n--- nested: a spacing scale ---");
const BASE = 16;
for (const n of [0, 1, 2, 3, 4]) {
  const value = BASE * Math.pow(1.5, n);
  console.log(`--space-${n}: ${px(value / ROOT)}rem = ${px(value)}px`);
}
--- clamp: font size by viewport width ---
font-size: clamp(1rem, 0.75rem + 1.2vw, 1.5rem)   (root 16px)
viewport  320px -> preferred 15.84px, applied 16px  (lower bound)
viewport  480px -> preferred 17.76px, applied 17.76px  (preferred)
viewport  640px -> preferred 19.68px, applied 19.68px  (preferred)
viewport  800px -> preferred 21.6px, applied 21.6px  (preferred)
viewport 1024px -> preferred 24.29px, applied 24px  (upper bound)
viewport 1440px -> preferred 29.28px, applied 24px  (upper bound)

--- calc: mixing different units ---
calc(100% - 2rem)       -> 608px   (containing 640px, root 16px)
calc(100% / 3)          -> 213.33px   (containing 640px, root 16px)
calc(100% / 3 - 16px)   -> 197.33px   (containing 640px, root 16px)
calc(50% + 20px)        -> 340px   (containing 640px, root 16px)
calc(2rem * 1.5)        -> 48px   (containing 640px, root 16px)

--- min and max: which one sets the bound ---
containing  320px -> min(90%, 600px)=288  max(90%, 600px)=600
containing  640px -> min(90%, 600px)=576  max(90%, 600px)=600
containing 1200px -> min(90%, 600px)=600  max(90%, 600px)=1080

--- clamp's equivalence with min/max ---
preferred=10 -> clamp=16  max(16,min(pref,24))=16  equal=true
preferred=16 -> clamp=16  max(16,min(pref,24))=16  equal=true
preferred=20 -> clamp=20  max(16,min(pref,24))=20  equal=true
preferred=24 -> clamp=24  max(16,min(pref,24))=24  equal=true
preferred=30 -> clamp=24  max(16,min(pref,24))=24  equal=true

--- nested: a spacing scale ---
--space-0: 1rem = 16px
--space-1: 1.5rem = 24px
--space-2: 2.25rem = 36px
--space-3: 3.38rem = 54px
--space-4: 5.06rem = 81px

clamp Is a Three-Zone Function

The first block shows the clamp() function’s three zones. At a 320-unit viewport, the preferred value calculated to 15.84 pixels, but 16 pixels got applied because it fell below the lower bound. From 480 to 800, the preferred value stayed within the range and got used directly. Past 1024, it hit the upper bound.

The fourth block confirms the equivalence: clamp(a, t, b) always gives the same result as max(a, min(t, b)). clamp() is not a separate mechanism, it is the name for this composition.

The writing 0.75rem + 1.2vw in the preferred value is a pattern: the sum of a fixed base and a component proportional to the viewport. If only vw got written, with no fixed component, the text would not grow when the user zoomed in — the problem mentioned in the previous lesson. The fixed component preserves that link.

calc Mixes Units

The second block shows calc()’s real job: percentage, rem, and px can get mixed in the same expression. This establishes relationships that could not get written any other way.

calc(100% - 2rem) — fill the containing block but leave two base units of space. calc(100% / 3 - 16px) — take a third of the width and subtract the gap between them.

There is one detail in the writing rules: a space is required on both sides of the + and - operators. The writing calc(100%-2rem) is invalid, because -2rem gets read as a negative number. A space is not required for * and /, but gets written for consistency.

A second constraint: in multiplication, at least one operand has to be a unitless number, and in division, the divisor has to be. Two lengths cannot get multiplied.

calc() can get written nested, and can also contain var() calls. Used together with custom properties, it lets a scale get derived from a single base — the fourth block gives the result of this.

min and max Set Bounds

The third block corrects a reading habit. min() sets an upper bound: the smallest of the given values gets chosen, so the result cannot exceed any of them. max() sets a lower bound.

The names looking like the opposite of the function is a frequently confused point. In the output, the writing min(90%, 600px) stopped at 600 once the containing block was 1200 units — that is, it acted as an upper bound.

These two functions have an advantage: they set a bound within the same declaration, without writing separate properties like min-width and max-width. They can also get used inside shorthand values — padding: min(5%, 2rem) can get written, whereas there is no max- property for padding.

The Course’s Style Sheet

Throughout the course, the station page’s style sheet grew step by step. Its wrapped-up form carries every decision built in this course:

/* station.css — wrapped up */

/* 1. Base */
*, *::before, *::after { box-sizing: border-box; }
:root { font-size: 100%; }

/* 2. Scales */
:root {
  --hue: 203;
  --brand-dark: hsl(var(--hue) 61% 20%);
  --surface:    hsl(var(--hue) 18% 97%);
  --line:       hsl(207 15% 86%);
  --text:       hsl(211 29% 16%);
  --text-muted: hsl(211 12% 45%);
  --warning:    hsl(0 66% 33%);

  --space-0: 1rem;
  --space-1: calc(var(--space-0) * 1.5);
  --space-2: calc(var(--space-1) * 1.5);
  --space-3: calc(var(--space-2) * 1.5);

  --font-body: clamp(1rem, 0.9rem + 0.35vw, 1.125rem);
  --font-h1:   clamp(1.75rem, 1.3rem + 2.2vw, 2.5rem);
}

/* 3. Document */
body {
  color: var(--text);
  font-family: "Station Sans", system-ui, sans-serif;
  font-size: var(--font-body);
  line-height: 1.5;
}

main {
  max-width: min(70ch, 100% - var(--space-1));
  margin-inline: auto;
  padding-block: var(--space-2);
}

main > * + *       { margin-block-start: var(--space-0); }
main > section     { margin-block-start: var(--space-3); }

/* 4. Masthead */
.masthead {
  padding: var(--space-1);
  margin-block-end: var(--space-2);
  background-color: var(--surface);
  border-block-end: 4px solid var(--brand-dark);
}
.masthead > :first-child { margin-block-start: 0; }
h1 { font-size: var(--font-h1); line-height: 1.15; }

/* 5. Measurement table */
.table-wrapper { overflow-x: auto; }
.measurement-table { width: 100%; min-width: 40rem; border-collapse: collapse; }
.measurement-table th,
.measurement-table td {
  padding-block: 0.5rem;
  padding-inline: 0.75rem;
  border-block-end: 1px solid var(--line);
}
.measurement-table thead th {
  position: sticky;
  z-index: 10;
  inset-block-start: 0;
  background-color: var(--surface);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
:where(.measurement-table) .value {
  text-align: end;
  font-variant-numeric: tabular-nums;
}
:where(.measurement-table) .missing { color: var(--warning); }
.measurement-table tbody tr:nth-child(odd) { background-color: var(--surface); }

/* 6. Location section */
.location-section { display: flow-root; }
.location-image {
  float: inline-start;
  inline-size: min(200px, 40%);
  margin-inline-end: var(--space-0);
  margin-block-end: 0.5rem;
}
.location-image img { max-width: 100%; height: auto; }
.source-note { clear: both; color: var(--text-muted); font-size: 0.875em; }

/* 7. States */
a:focus-visible,
input:focus-visible {
  outline: 3px solid var(--brand-dark);
  outline-offset: 2px;
}
input, select, textarea, button { font: inherit; color: inherit; }
input[required] { border-inline-start: 3px solid var(--warning); }
input:invalid   { border-color: var(--warning); }

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

The file is divided into seven sections, and the order is not random: base rules at the top, with the lowest specificity; component rules at the bottom. The order criterion established in the Cascade lesson makes this arrangement predictable — a conflict at the same specificity always resolves in favor of whichever gets written later.

The scales are defined in one place and derived from each other. The --space-1 value depends on --space-0; when the base changes, all the spacing values change together.

The main element’s width is the smaller of two constraints: a readable line length and the space to leave at the screen’s edge. This is the combination of the Box Sizing, Units, and Functions lessons.

Summary

  • calc() mixes different units in the same expression; a space is required on both sides of the addition and subtraction operators, and at least one operand in a multiplication has to be unitless.
  • min() sets an upper bound, max() sets a lower bound; even though the names look like the reverse of the functions, the chosen value is the smallest and the largest, respectively.
  • clamp(a, t, b) is the name for the composition max(a, min(t, b)) and behaves in three zones: the preferred value gets used if it is within range, and gets clamped to the relevant bound if it is outside.
  • In scaling typography, the preferred value gets written as the sum of a fixed base and a component proportional to the viewport; writing only a viewport unit breaks the zoom link.
  • When functions get combined with custom properties, a scale gets derived from a single base, and the entire scale can get shifted by changing a single value.

Course Wrap-Up

This course styled the measurement station document built in the Web Fundamentals and HTML curriculum, and did it in three layers.

The first topic established how rules reach the document: a declaration’s syntax, the set a selector returns, resolving conflicts through the cascade and specificity, and filling a value in by inheritance or its initial value. This topic’s distinguishing feature was that every question asked had a calculable answer: specificity is a triple, the cascade is an ordering, inheritance is a tree walk.

The second topic established how boxes get measured and arranged: the box model’s arithmetic, the numeric difference between the two sizing models, normal flow’s rules, margin collapsing, floating, positioning, and stacking context. The recurring lesson here was this: a layout problem first gets attempted with the rules of the flow; every declaration that pulls an element out of the flow comes with a cost.

The third topic tied visual decisions to a scale: color spaces and the contrast calculation, the modular typography scale, font-loading behavior, background layers, gradients, borders, units, and calculation functions. The principle here was building derived scales instead of individual values.

One thing the course did not cover remains: two-dimensional layout. In this course, boxes either stacked in the flow or got pushed aside by floating. Building columns, placing boxes on a grid, giving them gaps without collapsing, and adapting all of this to screen width is the job of a separate model.

The Layout Systems and Responsive Design curriculum builds this model. Two-dimensional placement with flexible box and grid layouts, responsive design with breakpoints derived from content, and organizing style architecture in a scalable way get taken up there. Everything learned in this course holds there too: the specificity calculation is the same calculation, the box model is the same model, the scales are the same scales. The only thing that changes is how the boxes get positioned relative to each other.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close