Units, custom properties and calc
Choosing between px, em, rem and viewport units, inheriting variables, registering them with @property, and doing arithmetic safely.
Pick a unit per job
A unit is a statement about what the value should follow. Sizing in rem tracks the user's font preference, in em tracks the local font size, and in dvh tracks the visible viewport on a phone with collapsing browser chrome.
| Unit | Relative to | Good for |
|---|---|---|
px | Nothing — a device-independent pixel | Hairlines, borders, tiny fixed offsets |
% | The parent's corresponding dimension | Fluid widths, but ambiguous height |
em | The element's own font size | Spacing that scales with its own text |
rem | The root font size | Type, spacing and sizing — the safe default |
vw / vh | The viewport | Large decorative type; avoid for height on mobile |
dvh / svh | Dynamic and small viewport height | Full-height heroes on phones |
ch | The width of a zero glyph | Measure — the line length of prose |
cqw | The nearest container's width | Component sizing inside container queries |
.card { padding: 1.5rem; }
.card :is(h2, p) { margin-block: 0.75em; } /* scales with each element's own size */
.hero { min-height: 100dvh; }
.prose { max-inline-size: 68ch; }em compounds: a 1.5em padding on a child of a child of a 20px element is not 20px. That compounding is useful for component-relative spacing and dangerous for nesting, which is why rem is the safer default.
Custom properties are inherited values
A custom property is not a variable in the programming sense: it is an inherited CSS value that is substituted into a declaration before that declaration is parsed. Inheritance is what makes theming work, and also what makes an undefined property silently resolve to nothing.
:root { --space-3: 1rem; }
.card { padding: var(--space-3, 1rem); } /* fallback if undefined */
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.spinner { transform: rotate(var(--angle)); }- Always provide a fallback with
var(--name, fallback)for values that come from outside the component. - Unregistered custom properties are untyped strings, so a typo produces an invalid value rather than an error.
@propertyregisters a type, an inheritance rule and an initial value — registered properties can be animated and transitioned.- Custom properties cannot be used in media query conditions, so keep breakpoints as literal values.
- JavaScript can set them per element with
element.style.setProperty("--angle", "45deg").
calc, min, max and clamp
Math functions let a layout describe a relationship instead of a fixed number, so it keeps working when a sidebar changes width or a gap gets larger.
:root {
--gap: 1rem;
--sidebar: 240px;
}
.layout {
display: grid;
gap: var(--gap);
grid-template-columns: var(--sidebar) 1fr;
}
.content {
max-inline-size: min(72rem, 100% - var(--sidebar) - 2 * var(--gap));
margin-inline: auto;
}
.title { font-size: clamp(1.5rem, 1rem + 2vw, 2.5rem); }calc()mixes units:100% - 240pxis legal, and is how you subtract a fixed sidebar from a fluid width.min()andmax()are the single-value form of clamp and read better when only one bound matters.clamp()is preferred when both bounds matter, as with fluid type.- Nesting is fine but gets unreadable fast — hoist repeated expressions into a custom property.
calc(), + and - must be surrounded by whitespace; calc(100%-2rem) is invalid and the whole declaration is dropped silently. Multiplication and division by a plain number do not have this restriction.FAQ
When should I use em instead of rem?
rem keeps things predictable.Why is my custom property not applying?
var() hides all three mistakes.Related
Typography, fonts and fluid type CSS architecture, naming and design tokens
Last refreshed 2026-09-18.