Under the Hood
Rendering

Computed style: from matched rules to final values

Winning the cascade only gives a property one candidate value, and that value still passes through several more processing stages — specified, computed, used, actual — before it becomes something layout can actually place a box with.

Computed style: from matched rules to final values

Lesson 4 ended with the cascade producing a single winning declaration for a property on an element — say, .card sets width: 50% and nothing else competes with it. It's tempting to treat that as the end of the story: the browser found the rule, so now it knows the width. It doesn't, not yet. 50% isn't a number of pixels, and the browser can't turn it into one until it knows what the element's containing block measures — which it won't know until layout actually runs. The cascade's output is a value, not the value. This lesson is the pipeline a property travels between those two points.

The value processing pipeline

For every CSS property on every element, the browser threads a value through up to five distinct stages, each one resolving something the previous stage left open:

Declared values. Every declaration from every matched rule that sets this property, before any conflict resolution — this is the raw input to the cascade from lesson 4.

Cascaded value. The single declaration the cascade picked as the winner: highest origin-and-importance tier, then highest specificity, then latest source order. If nothing matched at all, there is no cascaded value.

Specified value. If there's a cascaded value, that's it, unchanged. If there isn't — no rule touched this property on this element — the browser falls back: to the parent's computed value if the property inherits (like color), or to the property's own initial value from the CSS spec if it doesn't (like margin, which defaults to 0). Every property always ends this stage with some value; there's no such thing as "undefined."

Computed value. This is where the browser resolves everything it possibly can without running layout. Relative units like em and rem are resolved to an absolute px figure using the element's (or root's) font size. var() references to custom properties are substituted with the custom property's own value. Relative URLs in url(...) are made absolute. Some keywords are resolved to their long-hand equivalent. What's deliberately not resolved here: anything that depends on the size of another box — a percentage, or auto — because that box's size isn't known yet.

Used value. This is the value after layout has actually run. A width: 50% becomes a concrete pixel figure once the containing block's width is known; a height: auto on a block becomes the pixel height its content stacked up to. For properties that never depended on layout in the first place — color, font-weight — the used value is identical to the computed value.

Actual value. The used value adjusted to whatever the rendering environment can actually represent — most commonly, a fractional pixel value like 127.5px gets snapped to a whole device pixel the screen can draw.

.card {
  font-size: 1.5rem;        /* specified: 1.5rem */
  padding: 0.5em;           /* specified: 0.5em, relative to *this* element's font-size */
  width: 50%;               /* specified: 50%, relative to the containing block's width */
}

Assume the root font-size is 16px. The computed value of font-size resolves the rem immediately: 24px — no layout needed, since rem only depends on the root's font size, which is already known. padding's em also resolves at computed-value time, but only after font-size computes, since 1em here means "this element's own computed font-size": 12px. width: 50%, though, computes to... 50%. There's nothing to resolve it against yet. Its computed value is still a percentage; only during layout, once the containing block's width is settled, does it get a used value like 240px.

What getComputedStyle() actually returns

window.getComputedStyle(element) doesn't quite return "the computed value" in the strict sense above — despite the name, for layout-dependent properties it returns what the spec calls the resolved value, which is the used value once layout has happened to produce one, and falls back to the computed value only for properties that don't depend on layout at all.

const el = document.querySelector('.card');
const style = getComputedStyle(el);

style.fontSize; // "24px" — an em/rem already resolved to px, no layout dependency
style.width;    // "240px" — a resolved/used value: 50% turned into a real pixel number

That "240px" is exactly why reading getComputedStyle() (or offsetWidth, getBoundingClientRect(), and similar layout-reading properties) can silently force a layout recalculation if the browser hasn't run one since the last DOM or style change: there is no used value sitting in a cache waiting to be read, so the engine must go compute one right then, synchronously, before your script continues. Do this once between a write and the next frame and it's free. Do it in a loop that alternates writing a style and reading a layout value, and each iteration forces a full, synchronous layout pass — the failure mode the layout-thrashing lesson covers from the performance side. This lesson explains why that read is expensive: it's not the property lookup, it's the used-value computation the lookup triggers.

Custom properties resolve here too

CSS custom properties (--accent-color: teal;) are themselves ordinary inheriting properties, matched and cascaded exactly like any other — but their values are substituted into var() references at computed-value time, not before.

:root {
  --accent-color: teal;
}
.card {
  /* computed value: teal, substituted in during this element's computed stage */
  color: var(--accent-color);

  /* fallback only used if --missing-color was never declared anywhere up the tree */
  border-color: var(--missing-color, gray);
}

Because substitution happens per element at computed-value time, the same var(--accent-color) declaration can resolve differently on two different elements if a descendant redeclares --accent-color closer to it in the tree — the custom property inherits down, gets overridden partway, and each element's var() picks up whatever value was in force by the time its own computed value is worked out.

Where the computed style ends up

Every property on every element goes through this pipeline, and the result — the full set of computed (and where layout has run, used) values for one element — is exactly what pairs with that element's node in the render tree from the flagship lesson: render tree node plus computed style is the complete input layout needs to place a box. That's also the moment the still-unresolved percentages and auto values from this lesson finally get a used value — which is exactly where the next lesson picks up.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. List the five stages a property's value passes through, from every declared value to the actual value.
  2. What is the specified value of a property on an element that no rule ever matched — and does the answer depend on whether the property inherits?
  3. Why can width: 50% not get a computed value equal to a pixel number, while font-size: 1.5rem can?
  4. What does getComputedStyle() return for a layout-dependent property like width, and why is that not simply 'the computed value'?
  5. Explain, mechanically, why reading offsetWidth right after changing a style can force a synchronous layout.
  6. At what stage in the pipeline does a var(--x) reference get substituted with its custom property's value?
  7. What pairs with a render-tree node to give layout everything it needs to place a box?