Block Layout: Width, Height & Position

Building a Web Browser Engine: Layout & Rendering

Chapter 3 · Block Layout: Width, Height & Position

Chapter 2 built the machinery for one box's own dimensions. This chapter puts real numbers into it — a real, recursive algorithm that computes an actual width, an actual x/y position, and an actual height for every block box in a tree, in the right order, each one depending on the boxes around it exactly the way real CSS specifies.

The Four-Step Algorithm

def layout_block(layout_box, containing_block): calculate_block_width(layout_box, containing_block) calculate_block_position(layout_box, containing_block) layout_block_children(layout_box) calculate_block_height(layout_box)

Width has to be known before position (position depends on this box's own margin/border/padding, computed alongside width). Position has to be known before children are laid out (children need this box's own content-box coordinates as their own containing block). And height, for a box with no explicit height, can only be known after children — it's the sum of what they turned out to need.

Width: A Real Bug in the "Auto" Case

def calculate_block_width_naive(layout_box, containing_block): style = box_style(layout_box) padding = expand_shorthand(style.get('padding', '0')) border = expand_shorthand(style.get('border-width', '0')) margin = expand_shorthand(style.get('margin', '0')) width_str = style.get('width', 'auto') if width_str == 'auto': content_width = containing_block.content.width # BUG else: content_width = parse_length(width_str) layout_box.dimensions = Dimensions(Rect(0, 0, content_width, 0), padding, border, margin)

Reasonable-looking: when nothing sets an explicit width, just fill the container. But "fill the container" has to mean something specific — which box fills it?

Verified directly — a padded box overflows its own container by exactly its own padding
A 300px-wide containing block. A box with padding: 10px and no explicit width. The naive function sets its own content width to 300 — the same as the container. But that box's real outer edge is its border box, which adds the padding back on top: 300 + 10 + 10 = 320. The box's own visible edge ends up 20px wider than the container it's supposedly filling — a genuine, real overflow, verified directly by computing border_box() and comparing its width against the container's own.
def calculate_block_width(layout_box, containing_block): style = box_style(layout_box) padding = expand_shorthand(style.get('padding', '0')) border = expand_shorthand(style.get('border-width', '0')) margin = expand_shorthand(style.get('margin', '0')) width_str = style.get('width', 'auto') if width_str == 'auto': content_width = (containing_block.content.width - margin.left - margin.right - border.left - border.right - padding.left - padding.right) else: content_width = parse_length(width_str) layout_box.dimensions = Dimensions(Rect(0, 0, content_width, 0), padding, border, margin)
Verified directly — the fix makes the OUTER edge fill the container, not the content box
Same box, same 300px container: content width comes out to 280 (300 − 10 − 10), and border_box().width is exactly 300 — flush with the container, no overflow. It's the box's real, visible edge that fills the available space; the content box shrinks to make room for whatever padding, border, and margin the box itself carries.

Position and Height: One Box's Own Content Area Becomes Its Children's Container

def calculate_block_position(layout_box, containing_block): d = layout_box.dimensions cb = containing_block.content d.content.x = cb.x + d.margin.left + d.border.left + d.padding.left d.content.y = cb.y + cb.height + d.margin.top + d.border.top + d.padding.top def layout_block_children(layout_box): d = layout_box.dimensions for child in layout_box.children: layout_block(child, d) # THIS box's own dims -> child's containing block d.content.height += child.dimensions.margin_box().height def calculate_block_height(layout_box): style = box_style(layout_box) height_str = style.get('height', 'auto') if height_str != 'auto': layout_box.dimensions.content.height = parse_length(height_str) # else: leave it as whatever layout_block_children already accumulated

cb.y + cb.height is the whole stacking mechanism in one expression: a box's own y starts at its container's own top, plus however much vertical space the container has already accumulated from earlier siblings. layout_block_children grows that accumulated height by exactly one child's own margin_box().height every time a child finishes — so the next sibling's own position automatically lands below it.

Verified directly — a real, three-level nested layout, every number checked by hand
A 300px containing block holds box A (padding: 10px), which holds B (width: 100px; margin: 5px;) and C (height: 20px). Final results: A.content = Rect(10, 10, 280, 30), B.content = Rect(15, 15, 100, 0), C.content = Rect(10, 20, 280, 20), and A's own margin_box() = Rect(0, 0, 300, 50) — exactly as wide as the original container. B's content height comes out to 0 honestly — it has no children and no explicit height, so with no inline-content sizing built yet (a later chapter's own job), an empty block genuinely collapses to zero height, matching what a real browser does with a truly empty <div></div> too.

Where This Connects

This chapter's findingWhat it connects to
A box's own dimensions become its children's containing blockChapter 1's own layout tree — the recursive shape this chapter's algorithm walks is exactly the block/anonymous tree Chapter 1 built, with anonymous boxes handled identically to real block boxes via box_style()'s own INITIAL_VALUES fallback
"Auto" width filling the container's OUTER edge, not its content edgeChapter 4's own constraint-based width — this chapter only handles the single common case (width unset, margins not auto); the full puzzle of auto margins and over-constrained widths is that chapter's own entire subject
An empty block honestly collapsing to zero heightChapter 5's own inline layout — real height for boxes containing actual text will come from measuring that text, not from this chapter's own block-only stacking

Hands-On Exercises

Exercise 1

Build a container with three plain block children, each with only an explicit height set (15px, 25px, 5px) and everything else default. Lay it out inside a 300px-wide root containing block and confirm each child's own content.y — showing that the third child's own position depends on the running sum of both earlier children's heights, not just the immediately preceding one.

📄 View solution
Exercise 2

Build a container with a single child that has an explicit width: 50px. Lay it out inside a 300px containing block and confirm the child's own content width is exactly 50, completely independent of the container's own width — then explain in your own words why this is fundamentally different from the auto case, where the container's width genuinely does matter.

📄 View solution
Exercise 3

Reproduce this chapter's own overflow bug directly: lay out a box with padding: 10px and no explicit width inside a 300px containing block, using both calculate_block_width_naive() and calculate_block_width(). Compute border_box().width for both results and confirm the naive version overflows its own container while the fixed version doesn't — then identify exactly which line differs between the two functions.

📄 View solution

Chapter 3 Quick Reference

  • Order matters: width → position → children (recursively) → height — each step depends on the one(s) before it
  • Real bug found and fixed: naive "auto" width sets content width equal to the container's own width directly, ignoring this box's own padding/border/margin — the box's real outer edge (border box) then overflows the container by exactly that padding/border/margin amount
  • The fix: for "auto" width, subtract this box's own horizontal margin/border/padding from the container's width first, so the box's own outer edge — not its content edge — fills the available space
  • Position: cb.y + cb.height is the whole vertical-stacking mechanism — a box starts where its container's own already-accumulated height leaves off
  • Height: for an explicit height, use it directly; otherwise, sum every child's own margin_box().height — verified against a real three-level nested layout, checked field by field
  • Next chapter: Constraint-Based Width — the full "auto" puzzle when margins themselves can also be auto