CSS Variables and Functions
Chapter 9 — CSS Variables and Functions
CSS custom properties (commonly called CSS variables) are one of the most transformative
features in modern CSS. Unlike preprocessor variables (Sass $var, Less @var),
they exist at runtime in the browser, inherit through the DOM, can be read and written by
JavaScript, and respond to media queries. Combined with the growing CSS function library —
calc(), clamp(), min(), max(),
color-mix() and more — they enable design systems that adapt without
any tooling.
1. Custom Properties — Fundamentals
Declaration and use
--color: hello used in color: var(--color)), the browser
doesn't use the fallback — it uses the inherited value, or
initial if there is none. This is different from a missing variable
(which does use the fallback).
Inheritance — how custom properties flow down the DOM
Custom properties inherit like any other CSS property — a child element gets the value declared on its nearest ancestor. Redeclaring on a child overrides it for that subtree only.
2. Scoping Patterns
Global token scale
Component-scoped
Media-query override
Dark mode theming
Dark mode in practice — two themes, one set of rules
3. JavaScript Interop
:root via document.documentElement.style.setProperty()
triggers a global recalc. For high-frequency updates (mouse tracking, scroll),
scope the variable to a small subtree or use requestAnimationFrame
to batch updates.
4. @property — Typed Custom Properties
By default, CSS custom properties are untyped strings. The browser doesn't know if
--angle is a number, an angle, or a colour — it just substitutes the text.
This means you cannot transition or animate them. @property
registers a custom property with a type, initial value, and inheritance flag — unlocking
animation, validation, and IDE autocomplete.
Available syntax types
<length-percentage> to accept both.5. The CSS Function Library
Math functions — calc(), min(), max(), clamp()
clamp() visualised
clamp(1rem, 4vw, 2.5rem) — how font-size responds to viewport width
locked at 1rem (MIN)
scales with 4vw (PREFERRED)
locked at 2.5rem (MAX)
Full function reference
| Function | Category | Description + Example |
|---|---|---|
| calc() | Math | Arithmetic with mixed units. Supports +, -, *, /. Spaces required around + and -.
calc(100% - var(--sidebar-w) - 2rem)
|
| min() | Math | Returns the smallest argument. Accepts any number of comma-separated values.
width: min(100% - 2rem, 960px)
|
| max() | Math | Returns the largest argument. Useful for enforcing minimum values.
padding: max(1rem, 5vw)
|
| clamp() | Math | Equivalent to max(MIN, min(PREFERRED, MAX)). Clamps a fluid value between two bounds.
font-size: clamp(1rem, 2.5vw, 1.75rem)
|
| round() | Math | Rounds a value to the nearest multiple. Args: strategy (nearest/up/down/to-zero), value, interval.
width: round(nearest, 53.7px, 5px) → 55px
|
| mod() / rem() | Math | Modulo operations. mod() matches divisor sign; rem() matches dividend sign.
mod(18, 5) → 3
|
| abs() / sign() | Math | abs() returns magnitude; sign() returns -1, 0, or 1. Useful with custom properties for mirroring.
margin-left: abs(var(--offset))
|
| sin() cos() tan() asin() acos() atan() atan2() | Trig | Trigonometric functions. Input in radians or angle values. Used for circular layouts and wave effects.
--x: calc(cos(var(--angle)) * var(--radius))
|
| pow() sqrt() hypot() log() exp() | Math | Exponential functions. pow(base, exponent), sqrt(), hypot() = √(a²+b²). Useful for modular scale generation.
--step-2: calc(pow(1.25, 2) * 1rem)
|
| var() | Variable | Reads a custom property. Optional fallback as second argument.
color: var(--text-color, #c9d1d9)
|
| env() | Environment | Reads environment variables set by the browser. Most commonly used for iOS safe area insets.
padding-bottom: env(safe-area-inset-bottom)
|
| attr() | DOM | Reads an HTML attribute value for use in CSS content. Currently limited to content: in pseudo-elements; typed attr() with units is an emerging spec.
content: attr(data-label)
|
| color-mix() | Color | Mixes two colours in a specified colour space. Percentage controls the mix ratio.
color-mix(in oklch, #58a6ff 70%, #bc8cff)
|
| color-contrast() | Color | Selects from a list the colour with highest contrast against a base. (Emerging — limited support.)
color: color-contrast(#161b22 vs white, black)
|
| light-dark() | Color | Returns the first value in light mode, second in dark mode. Requires color-scheme to be set.
color: light-dark(#1a1a1a, #c9d1d9)
|
| oklch() oklab() lch() lab() | Color | Perceptual colour space functions. oklch(lightness chroma hue) for design-system tokens.
--brand: oklch(60% 0.22 250)
|
| hsl() hwb() rgb() | Color | Classic colour functions. Modern syntax drops commas and uses / for alpha.
hsl(220 80% 60% / 0.8)
|
| url() | Reference | References an external resource — image, font, SVG.
background: url('/images/bg.webp')
|
| format() local() | Font | Used inside @font-face src. format() hints the file type; local() checks for installed fonts.
src: local('Inter'), url('inter.woff2') format('woff2')
|
| linear() cubic-bezier() steps() | Easing | Custom easing functions for transitions and animations. Covered in Chapter 8.
transition-timing-function: cubic-bezier(0.34,1.56,0.64,1)
|
6. Full Design Token System
Chapter Summary
| Concept | Key point |
|---|---|
| var() fallback | Second argument is the fallback — used only when the property is not set, not when it holds an invalid value. Invalid values fall back to inherited or initial. |
| Inheritance scope | Custom properties inherit like any CSS property. Redeclaring on a child overrides for that entire subtree — no new selectors needed on descendants. |
| Unitless numbers | Store unitless numbers (--scale: 1.25) and multiply by a unit inside calc(). Allows JS to update the number cleanly without knowing the unit. |
| @property | Registers a custom property with a type, initial-value, and inherits flag. Required to animate a custom property. Must declare syntax and initial-value when inherits: false. |
| calc() gotcha | Spaces are mandatory around + and -. calc(100%+2rem) fails silently; calc(100% + 2rem) works. Multiplication and division don't need spaces. |
| min() for containers | min(100% - 2rem, 1200px) is the cleanest fluid container — no media query, always has 1rem margin on small screens, caps at 1200px on wide. |
| clamp() for type | clamp(MIN, preferred vw expression, MAX) creates fluid typography. The preferred value should use viewport units so it scales; min and max cap the extremes. |
| color-mix() | color-mix(in oklch, colorA 70%, colorB) mixes two colours. oklch gives perceptually uniform mixing — in hsl midpoints often go grey or muddy. |
| light-dark() | light-dark(light-value, dark-value) is a shorthand for two-value colour-scheme switching. Requires color-scheme: light dark on :root. |
| env() | Reads browser environment variables. safe-area-inset-* is essential for PWAs and mobile web apps on notched devices. |
| Three-layer tokens | Primitives → Semantic → Component. Only swap semantics for theming. Components reference semantics, never primitives directly. |
- Fluid type scale: Define five type scale steps on
:rootusingclamp(). Apply them to h1–h4 and body. Open DevTools device toolbar and drag the viewport width between 320px and 1400px — confirm the text scales smoothly with no jumps. - Component token pattern: Build a
.badgecomponent that exposes--badge-bg,--badge-color, and--badge-radius. Create three variants (.badge--success,.badge--warning,.badge--error) that only override the component-level tokens — no duplicating the structural rules. - Dark mode with data-theme: Set up a light/dark theme using semantic tokens (
--bg,--text,--surface,--accent). Implement both@media (prefers-color-scheme: dark)and a manual[data-theme="dark"]override. Add a button that toggles the attribute and persists the choice tolocalStorage. - @property animation: Register
--progressas a<percentage>withinitial-value: 0%. Build a progress bar that useswidth: var(--progress). Animate it from 0% to 100% over 3 seconds. Confirm it interpolates smoothly (it will fail without@property— test both ways). - color-mix() palette generator: Define a single brand colour as an oklch value. Generate tints and shades using
color-mix(in oklch, var(--brand) N%, white)andcolor-mix(in oklch, var(--brand) N%, black). Build a swatch row showing the full scale from 10% brand to 90% brand.