← Back to Index

Lesson 3

CSS Value Functions

CSS has a family of functions you can use inside property values to compute results at runtime. You've certainly seen calc(), but the full set - min(), max(), clamp(), and color-mix() - lets you build responsive, flexible designs that adapt without media queries.

calc() - arithmetic in CSS

calc() evaluates a mathematical expression at runtime. The killer feature: you can mix units.

/* Subtract a fixed sidebar from a fluid container */
.main { width: calc(100% - 250px); }

/* Add a rem offset to a viewport unit */
.hero { height: calc(100vh - 4rem); }

/* Multiply a custom property */
.box { padding: calc(var(--base-spacing) * 2); }

Operators: + - * /. Two rules:

Sidebar is 250px (fixed), main is calc(100% - 250px)

250px
calc(100% - 250px)
<!-- HTML -->
<div class="calc-demo">
  <div class="calc-sidebar">250px</div>
  <div class="calc-main">calc(100% - 250px)</div>
</div>

/* CSS */
.calc-demo {
  --sidebar: 250px;
  display: flex;
}

.calc-sidebar {
  width: var(--sidebar);
  flex-shrink: 0;
}

.calc-main {
  width: calc(100% - var(--sidebar));
}

Nesting calc() and combining with var()

You can nest calc() inside itself, though modern browsers also let you just use parentheses:

/* These are equivalent: */
width: calc(100% - calc(var(--sidebar) + 2rem));
width: calc(100% - (var(--sidebar) + 2rem));

Combined with custom properties, calc() becomes a mini expression language:

Padding computed from --base × --scale

My padding is calc(var(--base) * var(--scale)) = calc(1rem * 1.5) = 1.5rem

min() - the smaller of two or more values

min() takes a comma-separated list of values and resolves to the smallest. Think of it as a cap - "this big, but no bigger."

/* Width is 100% of parent, but never more than 500px */
.container { width: min(100%, 500px); }

/* Font size is 5vw, capped at 3rem */
.heading { font-size: min(5vw, 3rem); }

This box is min(100%, 500px) - resize the window to see it cap

I'll fill my parent up to 500px, then stop growing.
<!-- HTML -->
<div class="min-demo-box">I'll fill my parent up to 500px, then stop growing.</div>

/* CSS */
.min-demo-box {
  width: min(100%, 500px);
  padding: 1rem;
  background: #fff3e0; color: #1a1a2e;
  border-radius: 6px;
}

Mental model: min() sets an upper bound. "Be the smaller one" = "never exceed the larger one." It's like max-width but works anywhere, not just on width/height.

max() - the larger of two or more values

max() resolves to the largest value. It sets a floor - "at least this big."

/* Padding is 5% of container, but never less than 1rem */
.card { padding: max(5%, 1rem); }

/* Width shrinks with viewport, but never below 300px */
.sidebar { width: max(30vw, 300px); }

Mental model: max() sets a lower bound. "Be the bigger one" = "never go below the smaller one." It's like min-width but usable in any property.

clamp() - min, preferred, max in one

clamp(MIN, PREFERRED, MAX) is equivalent to max(MIN, min(PREFERRED, MAX)). It gives you a preferred value that can flex, with a floor and ceiling:

/* Font scales with viewport, clamped between 1rem and 2.5rem */
font-size: clamp(1rem, 2.5vw + 0.5rem, 2.5rem);

/* Width is 80% of parent, but between 300px and 800px */
width: clamp(300px, 80%, 800px);

/* Padding is fluid, with sensible bounds */
padding: clamp(1rem, 3vw, 3rem);

Resize the browser window - the text below scales fluidly between 1rem and 2.5rem

This text uses font-size: clamp(1rem, 2.5vw + 0.5rem, 2.5rem). It grows with the viewport but never gets too small or too large.
<!-- HTML -->
<div class="clamp-text">This text scales fluidly...</div>

/* CSS */
.clamp-text {
  font-size: clamp(1rem, 2.5vw + 0.5rem, 2.5rem);
  line-height: 1.3;
  padding: 1rem;
  background: #f9f0ff; color: #1a1a2e;
  border-radius: 6px;
}

The preferred value is usually a fluid unit - vw, %, or a calc() expression mixing viewport and fixed units. The min/max are usually fixed values (rem, px) that define the comfort zone.

Common pattern for responsive type: clamp(1rem, 0.5rem + 2vw, 2rem). The 0.5rem + 2vw gives you a sensible growth rate.

Reading clamp() - the three-part mnemonic

clamp( floor , preferred , ceiling )
       ↑            ↑           ↑
  never smaller   ideal     never bigger

Order matters: it's always min, preferred, max. If min > max, min wins (the floor always beats the ceiling).

Ad

color-mix() - blend two colours in CSS

Previously you needed Sass or JS to create colour variations. color-mix() does it natively:

/* Mix accent with white to lighten */
background: color-mix(in oklch, var(--accent), white 50%);

/* Mix accent with black to darken */
background: color-mix(in oklch, var(--accent), black 30%);

/* Mix two arbitrary colours */
border-color: color-mix(in oklch, red, blue 40%);

The syntax: color-mix(in <color-space>, <color1>, <color2> <percentage>?)

The percentage says how much of colour2 to mix in. white 50% means "50% white, 50% of colour1".

Colour spaces explained

The in <color-space> part tells the browser how to move between the two colours. Think of it like choosing a route on a map - the start and end are the same, but the path (and what you see along the way) changes depending on the space.

What is a colour space?

A colour space is a system for representing colours as numbers. Different spaces model colour differently:

Rule of thumb: Use in oklch for most mixing. It produces the most visually natural results because it respects how humans actually perceive colour. Fall back to in srgb if you need exact channel-level control or compatibility with legacy colour values.

Comparison: mixing red and blue in different spaces

/* Same mix, different paths */
--srgb:  color-mix(in srgb, red, blue 50%);   /* dark muddy purple */
--hsl:   color-mix(in hsl, red, blue 50%);    /* vivid magenta (hue rotation) */
--oklab: color-mix(in oklab, red, blue 50%);  /* balanced purple */
--oklch: color-mix(in oklch, red, blue 50%);  /* vibrant purple, preserves chroma */

Why are these different? Because the "midpoint" between red and blue is in a different location depending on which coordinate system you're navigating through:

Practical examples in each space

/* Lighten a brand colour - all valid, different results */
--light-srgb:  color-mix(in srgb, var(--brand), white 40%);
--light-hsl:   color-mix(in hsl, var(--brand), white 40%);
--light-oklab: color-mix(in oklab, var(--brand), white 40%);
--light-oklch: color-mix(in oklch, var(--brand), white 40%);

/* Create a semi-transparent overlay */
--overlay: color-mix(in srgb, black, transparent 50%);
/* ↑ srgb is fine here - we're just adjusting opacity, not mixing hues */

/* Blend two theme colours for a gradient midpoint */
--mid: color-mix(in oklch, var(--primary), var(--secondary) 50%);

/* Desaturate a colour (mix with its grey equivalent) */
--muted: color-mix(in oklch, var(--brand), gray 40%);

When to use which space

A tint scale from a single colour using color-mix(in oklch, accent, white/black)

80% white
50% white
base
30% black
60% black
/* Generate a full tint scale from one token */
:root { --accent: #3a86ff; }

--tint-100: color-mix(in oklch, var(--accent), white 80%);
--tint-300: color-mix(in oklch, var(--accent), white 50%);
--tint-500: var(--accent);
--tint-700: color-mix(in oklch, var(--accent), black 30%);
--tint-900: color-mix(in oklch, var(--accent), black 60%);

Practical win: define one brand colour as a custom property, then derive your entire palette using color-mix(). Change the brand colour and the whole palette updates. No build tools needed.

All of these compose together

The real power is combining these functions with custom properties:

:root {
  --min-font: 1rem;
  --max-font: 2.5rem;
  --fluid-rate: 2vw;
  --brand: #3a86ff;
}

h1 {
  font-size: clamp(var(--min-font), calc(0.5rem + var(--fluid-rate)), var(--max-font));
  color: color-mix(in oklch, var(--brand), black 20%);
  padding: max(1rem, 3vw);
}

Every value is responsive, bounded, and derived from tokens. No media queries required for the basics.

Quick reference

Function Returns Mental model
calc() Computed value Arithmetic, mix units
min() Smallest value Upper bound / cap
max() Largest value Lower bound / floor
clamp() Bounded preferred Floor + preferred + ceiling
color-mix() Blended colour Derive tints/shades from one token

Quiz

Question 1

What does min(100%, 600px) resolve to when the parent is 400px wide?

400px - 100% of 400px is smaller than 600px
600px - min() picks the static value
100% - percentages always win over px
500px - it averages the two values

Question 2

In clamp(1rem, 5vw, 3rem), what happens when 5vw computes to 4rem?

4rem - the preferred value is used as-is
3rem - it's capped at the maximum
1rem - preferred exceeds max so floor is used
5vw - viewport units are never clamped

Question 3

Why must + and - have spaces in calc()?

It's just a style convention, both work
The parser can't distinguish minus sign from negative number without spaces
Spaces activate hardware acceleration
Only - needs spaces, + works without them

Question 4

You want to lighten a colour by 40% using color-mix. Which is correct?

color-mix(in oklch, var(--c), lighten 40%)
color-mix(in oklch, var(--c), white 40%)
color-mix(oklch, var(--c), white 40%)
color-mix(var(--c), white, in oklch 40%)

Question 5

What colour space should you prefer for perceptually natural mixes?

srgb - it's the web standard
hsl - designed for human perception
oklch - perceptually uniform
rgb - most widely supported