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

Verified directly — every element has been defaulting to display:inline this whole course
Chapter 7's own 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

UA_STYLESHEET = Stylesheet([ Rule(['div', 'p', 'ul', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'], [Declaration('display', 'block')]), Rule(['span', 'b', 'i', 'a'], [Declaration('display', 'inline')]), Rule(['h1'], [Declaration('font-size', '2em'), Declaration('font-weight', 'bold')]), Rule(['b'], [Declaration('font-weight', 'bold')]), Rule(['p'], [Declaration('margin', '16px 0')]), ])

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.

Verified directly — applying only the UA stylesheet already fixes the display bug
A raw <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.

Verified directly — a higher-specificity UA rule beats a lower-specificity author rule that should have won
A UA rule 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.
def cascade_with_origin(element, ua_sheet, author_sheet): """Origin checked FIRST -- any matching author rule beats any matching UA rule, regardless of specificity. Specificity (then source order) only ever breaks a tie WITHIN the same origin.""" matches = [] # (origin, specificity, order, declarations) -- 0=UA, 1=author order_counter = 0 for origin, sheet in ((0, ua_sheet), (1, author_sheet)): for rule in sheet.rules: best_spec = None for sel_text in rule.selectors: sel = parse_selector(sel_text) if matches_selector(element, sel): s = specificity(sel) if best_spec is None or s > best_spec: best_spec = s if best_spec is not None: matches.append((origin, best_spec, order_counter, rule.declarations)) order_counter += 1 matches.sort(key=lambda m: (m[0], m[1], m[2])) result = {} for origin, spec, order, decls in matches: for decl in decls: result[decl.name] = decl.value return result
Verified directly — the fix resolves the exact case that broke, and holds up under a genuine tie too
Same two rules through 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 findingWhat it connects to
A real, built-in stylesheet supplying every tag's own default appearanceChapter 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 tierChapter 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 chapterA 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

Exercise 1

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.

📄 View solution
Exercise 2

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.

📄 View solution
Exercise 3

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.

📄 View solution

Chapter 9 Quick Reference

  • User-agent stylesheet: a real, ordinary Stylesheet the 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_VALUES alone gives every element display: 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