Default (User-Agent) Stylesheets & Real-World Fidelity
Building a Web Browser Engine: Parsing & the DOM
Chapter 9 · Default (User-Agent) Stylesheets & Real-World Fidelity
Open a real, completely un-styled HTML page — no <link>, no <style>, nothing — and it still doesn't look like a flat wall of text. Headings are big and bold. Paragraphs have space between them. <div> stacks vertically; <span> doesn't. None of that is magic, and none of it is hardcoded tag-by-tag logic somewhere deep in the engine. It's CSS — a real stylesheet, built into the browser itself, applied silently before a single author rule is ever considered.
The Bug Hiding in Plain Sight Since Chapter 7
INITIAL_VALUES sets 'display': 'inline'. Run a completely bare <div>, with no stylesheet at all, through compute_style(): the computed display comes back inline. Every single test in Chapters 6 through 8 that happened to care about layout-relevant properties simply never triggered this — but it's genuinely wrong. A real <div>, in every real browser, on a page with zero CSS anywhere, renders as a block.
CSS's own real initial value for display really is inline — that part of Chapter 7 was correct. What was missing is the second half of the picture: before any author stylesheet runs, the browser applies its own default stylesheet first, and that's where <div> actually picks up display: block.
A Real (If Simplified) User-Agent Stylesheet
It's built out of exactly the same Rule/Stylesheet shapes Chapter 4 defined — a UA stylesheet is a real stylesheet, not a special mechanism of its own. This particular set of rules is deliberately illustrative, not a verbatim copy of any real browser's own (much larger) default stylesheet — but the underlying idea it demonstrates is exactly how real browsers work.
<div>, run through compute_style() with only UA_STYLESHEET and no author rules at all, computes display: block. A raw <span>, same treatment, computes display: inline. Nothing else in this engine knows the difference between these two tags at all — the distinction lives entirely inside this one stylesheet.
A Real Bug: Specificity Alone Isn't Enough Once Two Origins Exist
The obvious next step — just concatenate UA_STYLESHEET.rules and the author's own rules into one combined Stylesheet, and run Chapter 6's own cascade() over it — looks reasonable. It even usually works, since most author rules naturally end up with equal or higher specificity than a simple UA default.
p.intro { margin: 40px; } — specificity (0,1,1) — against an author's own reset, p { margin: 0; } — specificity (0,0,1), genuinely lower. Concatenating both into one stylesheet and cascading by specificity alone: the UA rule wins, margin stays 40px. Real CSS never allows this — an author's rule always beats a UA rule, regardless of which one happens to have higher specificity. Specificity is only ever compared within the same origin, never across origins.
cascade_with_origin(): final margin is 0 — the author's reset wins outright. And separately: an author rule with the exact same specificity as a competing UA rule (both plain h1 selectors, (0,0,1) each) still resolves in the author's favor — origin is checked before specificity is ever consulted, so even a genuine specificity tie can't let a UA rule sneak through.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| A real, built-in stylesheet supplying every tag's own default appearance | Chapter 8's own style tree — display: none was already demonstrated there via an author rule; this chapter shows display: block/inline normally comes from the UA sheet instead, for every element, on every page, with or without any author CSS at all |
| Origin outranking specificity, checked as its own separate tier | Chapter 6's own cascade sort key, now needing a THIRD component ahead of the two it already had — (origin, specificity, order) instead of just (specificity, order) — the same "add one more tier to the same sorting idea" pattern that specificity itself was to source order |
| Chapter 7's INITIAL_VALUES being individually correct but incomplete without this chapter | A direct parallel to Chapter 5's own honest id-uniqueness gap and Chapter 3's <li> gap — a piece that was genuinely right for what it covered, revealed as incomplete only once a later chapter tests the case it never handled |
Hands-On Exercises
Run a raw <b> element through cascade_with_origin() using UA_STYLESHEET and a completely empty author stylesheet. Confirm the computed font-weight is still bold, and explain why origin-aware cascading produces the exact same result as plain cascading whenever only one origin has a matching rule at all.
Build an <h1> element. Give the UA stylesheet its usual h1 { font-weight: bold; } rule, and give the author stylesheet its own h1 { font-weight: normal; } rule — the exact same specificity, (0,0,1), on both sides. Run it through cascade_with_origin() and confirm the author's rule still wins despite the tie, then trace through the sort key to explain exactly which comparison decides it.
Reproduce this chapter's own margin bug directly: a UA rule p.intro { margin: 40px; } against an author rule p { margin: 0; }, on a <p class="intro"> element. Run it through both cascade_naive_combined() (the buggy concatenate-and-cascade approach) and cascade_with_origin(). Confirm the two functions disagree, and identify exactly which comparison inside cascade_naive_combined() is responsible for letting the UA rule win.
Chapter 9 Quick Reference
- User-agent stylesheet: a real, ordinary
Stylesheetthe browser applies before any author CSS — this is where<div>'s block-level default and<span>'s inline default actually come from, not a hardcoded tag check - Verified: Chapter 7's
INITIAL_VALUESalone gives every elementdisplay: inline— correct as CSS's own true initial value, but genuinely wrong for most real elements without a UA stylesheet supplying the rest - Real bug found and fixed: concatenating UA and author rules into one stylesheet and cascading by specificity alone lets a higher-specificity UA rule beat a lower-specificity author rule — real CSS checks origin (author always outranks UA) before specificity at all
- The fix: a three-part sort key,
(origin, specificity, order)— origin decides first; specificity and source order only ever break a tie within the same origin - Verified: even a genuine specificity tie between a UA rule and an author rule resolves in the author's favor, since origin is checked before specificity is ever compared
- Next chapter: Capstone — parsing a real HTML+CSS document, with the UA stylesheet applied, all the way into a fully-resolved style tree