---
title: 'CSS Functions'
source: 'https://academia.sh/en/courses/css-fundamentals/css-functions'
course: 'Visual Presentation with CSS'
language: en
updated: '2026-08-17T18:09:18+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```js
// 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:

```css
/* 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.
