Inheritance & Computed Values
Building a Web Browser Engine: Parsing & the DOM
Chapter 7 · Inheritance & Computed Values
Chapter 6's cascade() only ever returns properties that some rule actually set for that specific element. Most properties on most elements were never set by anything at all — and yet a real browser still shows a definite, specific value for every one of them. This chapter closes that gap: for every property, on every element, there has to be one final, unambiguous computed value, whether a rule mentioned it or not.
Two Fates for an Unset Property
CSS splits its properties into two groups. A fixed set of mostly text-related properties inherit — if nothing sets them directly, they take whatever value the parent element ended up with. Everything else falls back to a real initial value instead, completely independent of any ancestor.
This isn't arbitrary — it's why setting color once on <body> reliably colors every piece of text on a whole page, while setting margin on a container never leaks that same margin onto everything nested inside it. Box-model properties staying put on the element that set them is exactly what makes layout predictable at all.
A Naive First Attempt — and a Real Bug It Has
Reasonable-looking: borrow inheritable properties straight from the parent's own cascade() result. It even passes an obvious first test — a direct parent/child pair where the parent has a color rule.
id="g" setting color: red. A parent with no rule of its own. A child with no rule either. compute_style_naive(child, sheet) returns black — the initial default — not red. The parent's own direct cascade() result genuinely has no color entry at all (no rule matches the parent directly), so nothing gets borrowed from it, and the child never even looks past its immediate parent to find where the real value actually lives. A real browser shows this exact structure in red.
The Fix: Inherit From the Parent's Already-Computed Style
The critical difference: parent_computed is the parent's own fully resolved style — which already includes whatever it inherited from further up — not the parent's raw, direct cascade() result. That's only obtainable by walking the tree top-down and carrying each computed style downward as the recursion goes.
compute_style_tree(grandparent, sheet) gives grandparent color: red (its own rule), parent color: red (inherited from grandparent's computed style), and child color: red (inherited from parent's own computed style, which already includes what parent itself inherited). The value propagates transitively, exactly as far down the tree as nothing overrides it.
margin: 20px; color: green;. The child inherits color: green, but its margin comes out as the initial 0, completely unaffected by the parent's own 20px. Two properties, set together on the same rule, propagating in two entirely different ways.
color: red, a child with its own separate rule setting color: blue. The child's computed color is blue — base.update(own) runs after the inherited value is filled in, so a real matching rule on the element itself always has the final say.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| Top-down tree walk carrying each parent's computed style downward | Chapter 5's own assign_parents() — the same DOM tree, now walked in the opposite direction its structure was originally built to support, exactly as the descendant-selector matcher already needed to |
| A property's own final computed value being unambiguous for every element | Chapter 8's own style tree, which pairs every DOM node with exactly this — its own complete, resolved set of computed values, the direct input the layout engine in Course 2 will consume |
| Inherit-from-direct-cascade being a genuine, easy-to-miss bug | Chapter 6's own comma-selector bug — another case where a single, non-recursive parent lookup looks completely correct until tested against a tree more than one level deep |
Hands-On Exercises
Build a single element with no parent at all and an empty stylesheet (no rules whatsoever). Run it through compute_style_tree() and confirm its resulting computed_style dict is exactly equal to INITIAL_VALUES — nothing more, nothing less.
Build a three-generation chain (grandparent → mid → leaf). Give the grandparent a rule setting color, and give the middle element a separate rule setting a different inheritable property, font-weight — leave the leaf with no rule of its own. Confirm the leaf's computed style correctly picks up both inherited properties, each one traced back to a different ancestor.
Reproduce this chapter's own compute_style_naive() bug directly: a grandparent with a color rule, a parent with no rule, and a child with no rule. Confirm the naive version really does return the initial default instead of the inherited color. Then run the same three-generation tree through the fixed compute_style_tree() and confirm it returns the correct, inherited value instead.
Chapter 7 Quick Reference
- Two fates: a fixed set of text-related properties (
color,font-*,line-height, ...) inherit from the parent; everything else falls back to a real initial value - Real bug found and fixed: inheriting from the parent's own direct
cascade()result breaks two generations up — the fix walks the tree top-down, inheriting from the parent's own already-fully-resolved computed style instead - Verified: inheritance is transitive — a value can propagate through any number of ancestors that never set it themselves, as long as one of them inherited it in turn
- Verified: non-inherited properties (like
margin) never leak from parent to child, even when set on the same rule as an inheritable property that does - Verified: an element's own matching rule always overrides whatever it would otherwise have inherited
- Next chapter: Building the Style Tree — combining the DOM, the cascade, and inheritance into one single annotated tree