The anatomy of a rule showed where a value sits: after the colon, before the semicolon. This page looks inside that slot. It covers the types CSS checks, the units that make a design scale, the stages a value passes through, the functions that compute values, and the custom properties that let you name them. It closes with a map of the at-rules and the page that teaches each one.
1. CSS has a type system
Every value in CSS has a type, and every property accepts a specific set of types. The specifications define each property with a formal value grammar, and the browser checks what you write against it. Here is the definition of width from the CSS Sizing specification:
Read it like a function signature. A vertical bar means "or". Angle brackets name a type. The range in square brackets is a constraint: a width cannot be negative. Parentheses mark a function. So width takes the keyword auto, a non-negative length or percentage, one of three sizing keywords, or a call to fit-content(). The MDN page for every property opens with the same grammar, under "Formal syntax".
The types you will use most
| Type | Example values | Used for |
|---|---|---|
<length> | 1rem, 2ch, 50dvh, 4cqi | Sizes, distances, spacing |
<percentage> | 50% | Sizes relative to something the property defines |
<number> | 1.5, 0.75 | Ratios, line-height, opacity |
<integer> | 3, -1 | Grid lines, z-index, counters |
<color> | crimson, #c41e3a, oklch(55% 0.2 25) | Text, backgrounds, borders |
<image> | url(hero.avif), linear-gradient(…) | Backgrounds, list markers, masks |
<angle> | 45deg, 0.25turn | Rotations, gradient directions, hues |
<time> | 200ms, 0.4s | Transition and animation durations |
<string> | "Open Sans", "→" | Font names with spaces, generated content |
<custom-ident> | sidebar, fade-in | Names you choose: grid areas, animations, layers |
The specifications define dozens of types, but these cover most daily work. Two of them deserve a closer look.
Lengths are not numbers. 16 and 16px are values of different types, and the unit is part of the value. width: 16 is invalid. The single exception is zero: a length of 0 may drop its unit, because zero of anything is zero.
Numbers are not lengths either. line-height accepts both, and they behave differently: line-height: 1.5 means "one and a half times the element's own font size", while line-height: 1.5em becomes a fixed length. Section 3 shows why that difference matters for every descendant.
Type errors are silent
Write width: red and the browser parses red, finds a <color>, sees that width does not accept colors, and drops the declaration. Nothing appears in the console. This is the forgiving parsing that lets old browsers skip new syntax, seen from the other side: you will not be told about a type error, you will simply be ignored. DevTools is where you find out, since the Styles panel strikes through any declaration it rejected.
2. Units: absolute, relative, viewport and container
A length is a number with a unit, and the unit decides what the number is measured against. CSS has a great many units. The course deck called it the "unit explosion", and the list keeps growing. They fall into a few families, and the family matters far more than the individual unit.
What a CSS pixel really is
The px is not a dot on your screen. CSS defines a reference pixel: the size one pixel appears to be on a display of about 96 dots per inch, held at arm's length. On a phone with a high-density display, one CSS pixel is drawn with two or three device pixels in each direction. On a projector across a lecture hall, one CSS pixel may be a few millimeters wide. The browser reports the ratio as window.devicePixelRatio.
The other absolute units are defined from the pixel: 1in is 96px, 1pt is 1/72 of an inch, 1cm is about 37.8px. That means an "inch" on screen is only an inch by coincidence. Physical units make sense for print, where the page really is measured in centimeters or points, and almost nowhere else.
So the pixel is already an abstraction. Its real limitation is what it ignores. A length in px does not respond to the reader's preferred font size, does not know how wide the column is, and does not know how large the screen is. Page zoom still scales it, but the reader who sets a larger default font in the browser's settings gets nothing from a page sized in pixels.
Relative units
A relative unit resolves against something in the reader's context. That is exactly what makes it the right default: when the reader changes the context, the design follows.
font-size itself, em and % use the parent's font size.)| Unit | Relative to | Reach for it when |
|---|---|---|
rem | The root element's font size (by default, the reader's chosen size) | Type scales, spacing tokens, anything that should follow the reader's font setting |
em | The element's own font size; for font-size, the parent's | Padding, gaps and icons that should grow with the text beside them |
ch | The width of the "0" glyph in the element's font | Line length (max-inline-size: 65ch) and input widths |
lh | The element's computed line height | Vertical rhythm: space measured in lines of text |
ex | The x-height of the element's font | Fine typographic alignment, rarely |
% | A reference each property defines, usually the containing block | Fluid widths inside a parent |
vw, vh | 1% of the viewport's width or height | A fluid term inside clamp(); full-bleed sections |
vi, vb | 1% of the viewport in the inline or block direction | The logical versions of vw and vh |
svh, lvh, dvh | The small, large or dynamic viewport height | Full-height layouts on phones, where browser toolbars come and go |
vmin, vmax | The smaller or larger of vw and vh | Sizes that should fit either orientation |
cqi, cqb | 1% of the nearest query container's inline or block size | Components that scale with the slot they are placed in |
px | The reference pixel, 1/96 of an inch | A 1px hairline border, and little else |
Which of these units suit type, and why, is the subject of Size, spacing and measure in the Fonts section: rem for sizes, unitless line heights, and ch for line length.
Font-relative units: rem, em and ch
Browsers ship with a default font size, usually 16 pixels, and readers can change it. Because rem measures the root element, a page sized in rem grows when the reader asks for larger text. Spacing in rem grows with it, so the proportions of the design survive. This is the main reason rem is the house default.
em is local. It measures the element's own font size, so padding of 0.5em on a button is always half the button's text size, whether the button sits in a heading or a footnote. The catch is that font-size in em is relative to the parent, so it compounds: a nested list with li { font-size: 0.9em; } shrinks by ten percent at every level. Use em where you want that local scaling and rem where you want a fixed step on a global scale.
ch is the unit for reading. Lines longer than about 75 characters are tiring to track, and max-inline-size: 65ch sets the measure in the one unit that actually counts characters. Because ch follows the font, the measure stays right when the font or its size changes.
Percentages
A percentage is always a percentage of something, and each property says what. For inline-size it is the containing block's inline size. For font-size it is the parent's font size. For line-height it is the element's own font size. Two cases surprise people:
- Margin and padding percentages resolve against the containing block's inline size, even
padding-block. That is consistent, not a bug: it lets one percentage keep a box's proportions. block-size: 50%does nothing unless the parent's block size is definite. Heights normally come from content, so there is often nothing to take fifty percent of.
Viewport units, and the mobile toolbar
100vh was meant to be "the height of the screen". On phones, the browser's address bar slides away as you scroll, so the visible height changes. Browsers settled on the large viewport for vh, which made 100vh sections taller than the screen while the toolbar showed. The fix is three explicit units: svh for the small viewport (toolbars showing), lvh for the large one (toolbars hidden), and dvh for whichever is current. They are Baseline widely available. Prefer svh or dvh for full-height layouts, and keep a vh declaration before it if you want an older-browser fallback.
One more caution: on desktops with classic scrollbars, 100vw includes the scrollbar, so a 100vw box can be wider than the page and cause sideways scrolling. 100% of the body is usually what was meant.
Container units
Viewport units answer "how big is the screen". A reusable component needs to know how big its slot is. Container query units do that: once an ancestor declares container-type: inline-size, 1cqi is one percent of that container's inline size. With no container in sight, they fall back to the small viewport units. Container units are Baseline widely available, and the responsive design page pairs them with container queries.
Choosing, in practice
The deck observed that people reach for points and pixels far too often. That habit made sense when screens were all roughly the same; it does not anymore. Here is the habit in its classic form:
Every number there ignores the reader. The body text refuses the reader's preferred size, the fixed width overflows a phone, and the physical padding-left ends up on the wrong side in a right-to-left language. The same intent, measured against the reader's context:
The body font size is not set at all, so it is the reader's. The heading scales with the screen but keeps a rem floor and ceiling. The column is as wide as a comfortable line and never wider than the screen. This is how a style sheet holds up its end of the negotiation with the reader.
Units that are not lengths
The rest of the unit explosion belongs to other types. Angles use deg, turn, rad and grad. Times use s and ms. Resolutions, used in media queries and image-set(), use dppx (also written x), dpi and dpcm. Grid adds the flexible fr. Some units in older lists never really existed in browsers: Hz and kHz belonged to aural style sheets that were never implemented, and vm was Internet Explorer's name for what became vmin.
3. Specified, computed and used values
A value you write is not the value the browser paints. It passes through stages, and each stage resolves a little more of it:
- The specified value is the one the cascade chose for the property, as written:
1.5em,50%. If no rule set the property, it is the inherited value or the property's initial value. - The computed value resolves everything that can be resolved without laying out the page. Font-relative and viewport lengths become absolute lengths,
var()references are substituted, and keywords such asbolderbecome numbers. Percentages that depend on layout stay percentages. - The used value comes out of layout, when the sizes of boxes are known. Here
width: 50%finally becomes a number of pixels. - The actual value is what the screen can draw, after rounding to device pixels.
Why the stages matter
Inheritance passes computed values. A child that inherits from its parent receives the parent's computed value, not the text the author wrote. This explains the line-height puzzle from section 1:
Here every descendant inherits the number 1.5, so the h2 gets a line height of one and a half times its own large font. Change the article's value to 1.5em and it computes to a length, 24 pixels at the default size, before it is inherited. Now the heading's two-line titles overlap, because they inherit 24 pixels of line height for text that is 32 pixels tall. Unitless line-height is the safe default for exactly this reason.
The same rule explains why em compounds and rem does not: an em font size is resolved against the parent's computed size, which already carries every em above it. The cascade page follows the pipeline back through inheritance.
Scripts read resolved values. getComputedStyle() returns what the specification calls the resolved value. For most properties that is the computed value. For layout-dependent ones such as width and margin on a rendered element, it is the used value, in pixels. Asking for it forces the browser to finish layout first, which is one reason reading styles in a loop can be slow.
4. Functions
Anywhere a property accepts a value, it also accepts a function that returns a value of the right type. CSS ships a large function library: dozens of functions, from arithmetic to gradients to color. Each has a typed signature written in the same grammar as property values. Here is clamp() from the CSS Values specification:
The # means a comma-separated list, and {3} means exactly three. So clamp() takes three calculations. CSS functions are pure: no side effects, no state, no input or output. They are evaluated during the same pipeline as every other value, so a function's result can be computed at style time or, if it contains a percentage, at layout time.
calc() and dimensional arithmetic
calc() does arithmetic with the four usual operators and parentheses. What makes it interesting is that it respects types, much as physics respects units:
| Rule | Valid | Invalid |
|---|---|---|
| Add and subtract values of compatible types; lengths and percentages mix | calc(100% - 2rem) | calc(1rem + 2) |
| Multiply or divide by a plain number | calc(var(--space) * 3), calc(100% / 3) | calc(1rem * 1rem) |
Put spaces around + and - | calc(1rem + 1vw) | calc(1rem+1vw) |
The space rule exists because -1vw is itself a valid negative length, so the parser needs spaces to tell subtraction from a sign. The result of mixing a length and a percentage is a length-percentage that cannot be resolved until layout, which is why calc(100% - 2rem) is the classic way to say "the available space, minus an inset".
min(), max() and clamp()
These three take calculations as arguments, so they never need a calc() inside them.
min(a, b, …)returns the smallest argument: a ceiling.inline-size: min(100% - 2rem, 60rem)fills the space but never exceeds 60rem.max(a, b, …)returns the largest: a floor.inline-size: max(15rem, 30%)is never narrower than 15rem.clamp(min, preferred, max)returns the preferred value, held between the two bounds.
The pattern you will see most is fluid type: a size that grows smoothly with the viewport between a minimum and a maximum, with no breakpoints at all.
clamp() holds it between the floor and the ceiling, so the size is fluid only where fluid makes sense.Keep a rem term in the preferred value. Text sized in vw alone does not grow when the reader zooms or raises the default font size, because the viewport does not change. The rem part keeps the reader in control.
The rest of the math library
Beyond the four workhorses, CSS has a small scientific calculator. These take and return the same types as calc():
| Family | Functions | Used for |
|---|---|---|
| Stepping | round() | Snapping a value to an interval, rounding up, down or to the nearest |
| Remainders | mod(), rem() | Repeating patterns; the two differ in the sign of the result |
| Sign | abs(), sign() | Distances and directions |
| Trigonometry | sin(), cos(), tan(), asin(), acos(), atan(), atan2() | Placing items around a circle; decorative geometry |
| Exponential | pow(), sqrt(), hypot(), log(), exp() | Modular type scales; distances |
The trigonometric, exponential and stepping functions reached every engine in 2023 and 2024. abs() and sign() came last: Chrome added them in mid-2025, so they have been Baseline newly available only since then. A modular type scale shows how they combine with custom properties:
Color functions, in brief
Color has more notations than any other type. Named colors, hex codes, rgb() and hsl() are the historical ones. oklch() describes a color by lightness, chroma and hue in a perceptually uniform space, so two colors with the same lightness look about equally bright, which hsl() cannot promise. Three functions build colors from other colors:
color-mix(in oklch, var(--accent), white 20%)blends two colors in a chosen space;- relative color syntax,
oklch(from var(--accent) calc(l + 0.1) c h), takes a color apart into channels and rebuilds it; light-dark(oklch(25% 0.02 250), oklch(92% 0.01 250))picks the first color in a light color scheme and the second in a dark one.
oklch() and color-mix() are Baseline widely available; relative colors and light-dark() are newly available. The Color section teaches them in full: Color in CSS for every notation, and Deriving color for mixing and relative colors.
The other families
Most remaining functions live where they are used, and other pages teach them in context: gradients and url() for images; translate() and rotate() for transforms; blur() and friends for filters; minmax(), repeat() and fit-content() for grid layout; counter() for generated content; and cubic-bezier() and steps() for easing. Three more read values from outside the style sheet: var() reads a custom property (next section), attr() reads an HTML attribute (as a string in every browser; Chromium-based browsers since 2025 can also read it as a typed value such as a length, which is not yet Baseline), and env() reads a value the browser supplies, such as env(safe-area-inset-bottom) for the area a phone's rounded corners or home indicator cover.
The boundary of the language
For most of its history CSS had a fixed library: if a function was not in the specification, you needed a preprocessor such as Sass or a script. That boundary is moving. A custom-functions specification adds @function, which lets an author define a function that returns a value, and a separate if() function chooses between values inline. Both have shipped in Chromium browsers, but neither was Baseline when this page was written, so treat them as experiments and keep a plain declaration first as a fallback.
Meanwhile, a pattern that works everywhere covers most practical cases: a rule reads its parameters from custom properties with defaults, and the caller sets the ones it cares about.
That is not a real function: there is no return value and no encapsulation. But it is the right instinct. CSS is a declarative language for describing presentation, not a general-purpose one, and its growth comes from adding precise, typed tools to that job.
5. Custom properties and var()
CSS has variables. The specification calls them custom properties, and the name is exact: they are properties, declared like any other, with a name that begins with two hyphens. You read one with the var() function.
Names are case-sensitive, so --Accent and --accent are different properties. Any name is allowed after the hyphens; pick names for the role a value plays (--accent, --space) rather than its appearance (--orange, --16), so a redesign changes values without renaming them.
Scope is the document tree
Custom properties inherit. A value declared on :root, the html element, reaches every element in the page. A value declared on any other element reaches that element and its descendants, and overrides the outer value there. There is no block scope and no closure: the scope chain is the document tree.
Every var(--accent) inside the footer, including the link rule from the previous example, now resolves to the lighter color. Nothing else had to change.
Fallbacks
var() takes an optional second argument, used when the property is not defined. Fallbacks can nest:
Read it as "use --link-pad; if that is not set, --space; if neither is, 0.5rem". Fallbacks let a component work on a page that has not defined the tokens it hopes for.
Untyped tokens, checked late
An ordinary custom property is not checked when it is declared. Its value is just a sequence of tokens, stored as written and substituted wherever var() asks for it. The check happens at the place of use, at computed-value time:
The first rule is perfectly legal. The second declaration parses, because the browser cannot know what --gap holds until it substitutes it. When it does, wide is not a length, and the declaration becomes invalid at computed-value time. It is too late to fall back to an earlier gap declaration in the cascade, so the property acts as if it were unset: inherited if it normally inherits, otherwise its initial value. Here gap becomes 0.
Late substitution has a useful side. Because an unregistered property is stored as tokens, an em inside it resolves where it is used:
The heading gets half of its large font; the small element gets half of its small one. One token, scaled to each context.
Live values
Preprocessor variables are replaced by values when the style sheet is built, then gone. Custom properties exist in the running page. Change one, with a selector or a script, and every element that reads it updates at once. That is what makes theming simple:
A script that sets document.documentElement.dataset.theme = "dark", or element.style.setProperty("--space", "2rem"), changes data, not presentation. The style sheet still decides what the data means. That division, which Basics recommended for inline styles, keeps scripts and CSS from fighting over the same declarations.
6. Typed custom properties with @property
Untyped properties have two weaknesses. Mistakes are not caught where they are made, and the browser cannot animate them: if it does not know whether --fill holds a length, a color or a word, it cannot work out the values halfway between two settings, so a change simply jumps. The @property at-rule fixes both by registering a type.
The three descriptors are the type, whether the property inherits, and its starting value. The starting value must be computationally independent: the browser has to be able to resolve it without looking at any element. 0% and 0 qualify; 1rem does not, because it depends on the root font size, and an invalid initial value makes the browser drop the whole @property rule. Register with a neutral initial value and set the real one where it belongs, such as :root { --gap: 1rem; }. Registration changes three things:
- Values are checked when declared.
--fill: wideis now an invalid declaration, dropped at parse time like any other type error, so an earlier valid value wins. - There is always a value.
var(--fill)resolves to the initial value even if nobody set it. - The property can be animated. The browser knows how to interpolate percentages, so a transition on
--fillruns smoothly.
The third point is the one that opens new effects. Gradients cannot be transitioned directly, but a typed property used inside a gradient can:
Hovering a link draws its underline across. Without the registration, the underline would snap from empty to full. The transition is added only for readers who have not asked for reduced motion; everyone else still gets the underline, just without the movement.
Details that trip people up
- The
syntaxstring uses the type names from section 1, in quotes:"<length>","<color>","<number>","<angle>", unions such as"<length> | auto", and"*"for any tokens. syntaxandinheritsare required, and so isinitial-valueunless the syntax is"*".- The initial value must be computationally independent: the browser must be able to compute it without any element's context.
0%,0degand a color are fine;1emand1remare not, and make the whole rule invalid. - A registered length computes to an absolute length where it is declared, so the use-site scaling trick from the previous section no longer applies. Register a property when you want checking and animation; leave it unregistered when you want late, flexible substitution.
inherits: falsesuits values that are local to one element, such as an animation's progress, and saves the browser from passing them down the tree.
Scripts can register the same property with CSS.registerProperty():
@property is Baseline newly available since 2024. In a browser without it, the rule is ignored and the property behaves like an ordinary untyped one: the underline still works, it just appears without animating. That is progressive enhancement by default.
7. At-rules at a glance
Some of the most important parts of CSS are not rules but at-rules: @layer, @media, @property. Every one shares a single shape. After the @ and its name comes an optional prelude, then either a semicolon or a block. The block holds one of two kinds of content.
A group at-rule holds ordinary rules and applies them conditionally or organizes them: rules inside @media apply only when the media query matches. A descriptor at-rule declares a named object, and its block holds descriptors, which look like declarations but are not properties. syntax inside @property and src inside @font-face mean nothing anywhere else. @keyframes is the odd one out: its block holds keyframe blocks, each a percentage with its own declarations.
Most at-rules are taught where they are used. This table is the map:
| At-rule | Body | What it does | Taught in |
|---|---|---|---|
@import | statement | Pulls in another style sheet, optionally into a layer or under a condition | Basics |
@layer | statement or rules | Declares the order of cascade layers, or fills one | The cascade |
@scope | rules | Limits selectors to a subtree, with an optional lower bound | The cascade |
@media | rules | Applies rules for a medium, a viewport size or a reader preference | Basics, then Responsive |
@container | rules | Applies rules when an ancestor container matches a query | Responsive |
@supports | rules | Applies rules when the browser supports a feature | Responsive |
@page | descriptors and margin boxes | Sets the size and margins of printed pages | Responsive (print) |
@property | descriptors | Registers a typed custom property | This page |
@function | function body | Defines a custom function (not yet Baseline) | This page, in brief |
@counter-style | descriptors | Defines a list-marker style | Properties |
@font-face | descriptors | Loads a web font and names it | The Fonts topic (coming) |
@font-palette-values | descriptors | Overrides the palette of a color font | The Fonts topic (coming) |
@keyframes | keyframe blocks | Defines an animation as a sequence of states | The Animation topic (coming) |
@starting-style | rules | Gives an element a starting point for entry transitions | The Animation topic (coming) |
@namespace, @charset | statement | XML namespaces and the file's encoding; rarely needed in HTML | Not covered |
Group at-rules nest inside each other and, with native nesting, inside style rules too. That is how the examples on this page put a @layer inside a @media preference query: the enhancement stays in the layer it belongs to, and only applies when the reader's settings allow it.
8. Summary
| Concept | Key points |
|---|---|
| Types | Every value has a type, and each property's grammar lists the types it accepts. Lengths need units (except zero). Type errors are dropped silently; DevTools shows them. |
| The CSS pixel | A reference pixel of about 1/96 inch at arm's length, not a device dot. Physical units derive from it. Pixels ignore the reader's font setting; use them for hairlines. |
| Relative units | rem follows the reader's font size, em the element's, ch sets line length, % follows a property-defined reference, dvh and svh handle mobile toolbars, and cqi follows the component's container. |
| The value pipeline | Specified, computed (relative lengths made absolute, var() substituted), used (percentages resolved by layout), actual. Inheritance passes computed values, so use unitless line-height. |
| Math functions | calc() respects types. min() is a ceiling, max() a floor, clamp() both. Keep a rem term in fluid sizes so zoom still works. |
| Color functions | oklch() for perceptual color; color-mix(), relative colors and light-dark() derive colors from tokens. Full treatment in the Color topic. |
| Custom properties | Named with --, read with var() and its fallback, scoped by inheritance through the document tree, and live at run time. Untyped values are checked only where they are used. |
@property | Registers a type, inheritance and initial value, so values are checked early, always defined, and animatable. Initial values must not depend on context. |
| At-rules | One shape: name, prelude, then a semicolon, a block of rules or a block of descriptors. Each is taught on the page where it is used. |