From Style Tree to Layout Tree

Building a Web Browser Engine: Layout & Rendering

Chapter 1 · From Style Tree to Layout Tree

Course 1 ended with a fully-resolved StyledNode tree — every DOM node paired with its own final computed style. That tree still doesn't know how big anything is, or where anything sits. This course's own job is to answer that, and it starts here: converting the style tree into a second, related-but-genuinely-different tree — the layout tree — shaped specifically for the algorithms the rest of this course builds.

The LayoutBox: Block, Inline, or Anonymous

class LayoutBox: def __init__(self, box_type, styled_node=None): self.box_type = box_type # 'block' | 'inline' | 'anonymous' self.styled_node = styled_node # None for anonymous boxes -- they came from self.children = [] # no single DOM node at all def display_type(styled_node): """A text node is always inline -- it can never itself be a block container. An element uses its own computed 'display' value.""" if styled_node.node.kind == 'text': return 'inline' return styled_node.style.get('display', 'inline')

A Naive First Attempt — and the Rule It Silently Breaks

def build_layout_tree_naive(styled_node): box = LayoutBox(display_type(styled_node), styled_node) for child in styled_node.children: box.children.append(build_layout_tree_naive(child)) return box

A plain 1:1 mapping — every style-tree node becomes exactly one LayoutBox, in exactly the same shape. Every later chapter in this course, though, is going to need one specific guarantee: a block box's own direct children never include a raw, unwrapped inline box. Block layout (Chapter 3) stacks its children vertically — that only works if every direct child genuinely behaves like a block. Real, ordinary HTML routinely violates this on the surface.

Verified directly — plain text sitting next to a real block element breaks the invariant immediately
A <div> whose content is Hello <b>world</b>, welcome!\n<p>Second paragraph.</p>\ntail text. Its style-tree children are, in order: a text node, <b>, another text node, <p>, and a final text node. Run through build_layout_tree_naive, the div's own LayoutBox children come out as ['inline', 'inline', 'inline', 'block', 'inline'] — three separate inline boxes and one block box, sitting as direct siblings of each other. A real block-stacking algorithm handed this tree has no consistent rule to apply: some children are individually-sized blocks, some are inline runs that should flow together on a shared line — and nothing in the tree says which is which without re-deriving it every time.

The Fix: Group Consecutive Inline Runs Into One Shared Anonymous Box

def build_layout_tree(styled_node): box_type = display_type(styled_node) box = LayoutBox(box_type, styled_node) if box_type != 'block': # an inline box's own children are handled by a later chapter's own # line-box logic -- just recurse plainly, nothing to group here for child in styled_node.children: box.children.append(build_layout_tree(child)) return box pending_inline = [] def flush(): if pending_inline: anon = LayoutBox('anonymous', None) anon.children = pending_inline[:] box.children.append(anon) pending_inline.clear() for child_styled in styled_node.children: child_type = display_type(child_styled) if child_type == 'block': flush() box.children.append(build_layout_tree(child_styled)) else: pending_inline.append(build_layout_tree(child_styled)) flush() return box
Verified directly — three leading inline siblings merge into ONE shared anonymous box, not three separate ones
Same div. The fixed tree's own children come out as ['anonymous', 'block', 'anonymous'] — the text/<b>/text run collapses into a single anonymous box containing all three, <p> stays a real block box, and the trailing tail text after <p> gets its own separate anonymous box, since a real block sibling came between the two runs. Grouping is per contiguous run, not a single blanket wrapper for the whole container.
Verified directly — an inline box's own children are never grouped, and a purely-block container gets zero anonymous boxes
<b>'s own text child stays a plain inline child, untouched — grouping only ever applies to a block box's own direct children. And a <div> containing only <h1> and <p>, with no stray inline content at all, produces exactly ['block', 'block'] — no anonymous boxes appear when there's genuinely nothing inline to wrap.

The Real Document, Revisited

Course 1's own capstone document had ordinary line breaks in its source between <h1>, <p>, and <ul> — and that chapter's own closing finding noted these survive as real, if invisible, whitespace-only text nodes in the style tree. Run through this chapter's own machinery, that finding turns out to be exactly the problem this chapter exists to solve.

Verified directly — the real capstone document has this exact problem, not just the synthetic example
#page's own real style-tree children (built by Course 1's actual, unmodified pipeline) come out as ['inline', 'block', 'inline', 'block', 'inline', 'block', 'inline'] — four whitespace-only text nodes sitting directly between three real block elements. The naive layout tree built from this real document fails the same invariant check the synthetic example failed. The fixed build_layout_tree resolves it identically: every child becomes block or anonymous, with each isolated whitespace run wrapped in its own single-child anonymous box.

Where This Connects

This chapter's findingWhat it connects to
A block box's own direct children must never mix raw block and inline typesChapter 3's own block-layout algorithm, which will simply iterate over a block box's own children and stack each one vertically — a guarantee this chapter's own grouping step is what makes that safe to do unconditionally
Anonymous boxes exist purely to satisfy that invariant, with no DOM node of their ownChapter 5's own inline layout and line-box building — an anonymous box is exactly where a single inline formatting context (one shared line-flowing region) will actually get built
Whitespace-only text nodes from Course 1's own capstone turning out to be a real instance of this problemCourse 1 Chapter 10's own closing bonus finding, explicitly forward-referenced there as "Course 2's own inline-layout whitespace handling" — this chapter is the first half of that promise, grouping the whitespace; a later chapter decides what to actually do with it during real line-breaking

Hands-On Exercises

Exercise 1

Build a single <span> style-tree node with one text child and no block descendants anywhere. Run it through build_layout_tree() and confirm the resulting box's own box_type is 'inline' and its own child is also 'inline', un-grouped — then explain why this case never even reaches the grouping logic at all.

📄 View solution
Exercise 2

Build a block container whose children are two separate inline runs, split by one real block sibling in between: text + <b> (run one), a <p>, then a lone trailing text node (run two). Run it through build_layout_tree() and confirm you get exactly two separate anonymous boxes — not one combined box spanning the whole container — and that the first anonymous box's own children count is 2 while the second's is 1.

📄 View solution
Exercise 3

Run Course 1's own real capstone document (#page, containing <h1>, <p class="intro">, and <ul>, with real whitespace between each) through both build_layout_tree_naive() and build_layout_tree(). Confirm check_block_container_invariant() returns False for the naive tree and True for the fixed one, and count how many separate anonymous boxes the fixed version produces for #page's own direct children.

📄 View solution

Chapter 1 Quick Reference

  • LayoutBox: box_type ('block' / 'inline' / 'anonymous'), an optional styled_node (None for anonymous boxes), and a list of child LayoutBoxes
  • The invariant every later chapter relies on: a block box's own direct children never include a raw, unwrapped inline box
  • Real bug found and fixed: a plain 1:1 style-tree-to-layout-tree mapping lets block and inline boxes sit as direct siblings whenever stray text or an inline element appears next to a real block child — grouping consecutive inline runs into shared anonymous boxes fixes it
  • Verified: grouping is per contiguous run (not a single blanket wrapper), inline boxes' own children are never grouped, and a purely-block container produces zero anonymous boxes
  • Verified against the real document: Course 1's own capstone whitespace-text finding turns out to be exactly this problem, confirmed on the real pipeline output, not just a synthetic example
  • Next chapter: The Box Model — content, padding, border, and margin, and computing each box's own real dimensions from its computed style