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
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
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?
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.
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
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.
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 finding | What it connects to |
|---|---|
| A box's own dimensions become its children's containing block | Chapter 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 edge | Chapter 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 height | Chapter 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
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.
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.
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.
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.heightis 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 ownmargin_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