The Box Model: Content, Padding, Border, Margin

Building a Web Browser Engine: Layout & Rendering

Chapter 2 · The Box Model: Content, Padding, Border, Margin

Every real element on a page occupies four nested rectangles, not one: a content box, wrapped by padding, wrapped by a border, wrapped by margin. Every later chapter in this course computes real pixel positions by growing one of these boxes outward into the next. Getting the arithmetic and the CSS shorthand parsing genuinely right here is what everything downstream depends on.

Scope note
This course only implements box-sizing: content-box — the default, where an element's own width/height describe the content box specifically, and padding/border/margin all add on top of that. box-sizing: border-box (where width instead describes the outer, padding-and-border-inclusive box) is a real, common CSS feature — deliberately out of this course's own stated scope.

Four Boxes, Grown Outward One at a Time

class Rect: def __init__(self, x, y, width, height): self.x = x; self.y = y; self.width = width; self.height = height class EdgeSizes: def __init__(self, top, right, bottom, left): self.top = top; self.right = right; self.bottom = bottom; self.left = left def expand_rect_by(rect, edge): """Grows a rect OUTWARD by an EdgeSizes -- the one mechanism behind every step from content -> padding -> border -> margin box.""" return Rect( rect.x - edge.left, rect.y - edge.top, rect.width + edge.left + edge.right, rect.height + edge.top + edge.bottom, ) class Dimensions: def __init__(self, content, padding, border, margin): self.content = content # a Rect -- the CONTENT box only self.padding = padding # EdgeSizes self.border = border # EdgeSizes self.margin = margin # EdgeSizes def padding_box(self): return expand_rect_by(self.content, self.padding) def border_box(self): return expand_rect_by(self.padding_box(), self.border) def margin_box(self): return expand_rect_by(self.border_box(), self.margin)

Every one of the three outer boxes is defined purely in terms of the box directly inside it — the same one expand_rect_by function, called three times in sequence, is the entire box model. This is exactly what a real browser's own DevTools "box model" panel visualizes: content, padding, border, and margin as four nested rectangles, each one measurably bigger than the last.

Bug 1: A Length Parser That Crashes on CSS's Own Valid Zero

def parse_length_naive(s): return float(s[:-2]) # assumes every length always ends in 'px'
Verified directly — and it's not hypothetical, it's already in this course's own published UA stylesheet
parse_length_naive('16px') works fine — 16.0. But parse_length_naive('0') raises ValueError: could not convert string to float: '''0'[:-2] on a one-character string is the empty string, and float('') fails. CSS explicitly allows a bare, unitless 0 (every other length needs a unit, but zero never does) — and Chapter 9's own published UA_STYLESHEET already contains exactly this shape: Rule(['p'], [Declaration('margin', '16px 0')]). Parsing that real, already-existing rule's own value with the naive parser crashes immediately.
def parse_length(s): s = s.strip() if s == '0': return 0.0 if s.endswith('px'): return float(s[:-2]) raise ValueError(f"unsupported length: {s!r}")
Verified directly — the fix resolves the exact real value that broke
parse_length('16px 0'.split()) — parsing each token of Chapter 9's own real margin value separately — gives [16.0, 0.0], correctly.

Bug 2: Shorthand Expansion Getting the 3-Value Form Wrong

CSS's own margin/padding/border-width shorthand accepts 1, 2, 3, or 4 space-separated values, each shape meaning something genuinely different.

def expand_shorthand(value_str): lengths = [parse_length(p) for p in value_str.split()] if len(lengths) == 1: t = r = b = l = lengths[0] elif len(lengths) == 2: t = b = lengths[0]; r = l = lengths[1] elif len(lengths) == 3: t, r, b = lengths l = r # left REUSES the horizontal value elif len(lengths) == 4: t, r, b, l = lengths # clockwise from the top return EdgeSizes(t, r, b, l)
Verified directly — a plausible naive version reuses the WRONG earlier value for the 3-value case
A naive first attempt at the 3-value branch — t, r, b = lengths; l = t — reaches for the top value as left's own fallback, since it's the first one already in scope. expand_shorthand_naive("10px 20px 30px") gives EdgeSizes(top=10, right=20, bottom=30, left=10). Real CSS's 3-value form means (top, horizontal, bottom)left is supposed to reuse the horizontal (second) value, 20, matching right, not the top value. The fixed version's l = r gets this right: EdgeSizes(top=10, right=20, bottom=30, left=20).
Verified directly — all four shorthand forms behave correctly
expand_shorthand("5px") → all four sides equal, 5 each. expand_shorthand("10px 20px") → vertical 10, horizontal 20. expand_shorthand("1px 2px 3px 4px") → top/right/bottom/left exactly as written, clockwise from the top.

A Real Dimensions Computation, Box by Box

A content box 100×50 at the origin, padding: 10px, border-width: 2px, margin: 16px 0 — the exact margin shape from Chapter 9's own real UA rule.

Verified directly — every box grows exactly as much as the arithmetic predicts
Content: Rect(0, 0, 100, 50). Padding box: 10px on all sides → Rect(-10, -10, 120, 70) (width/height each grow by 20 — 10 on both sides). Border box: a further 2px all sides → Rect(-12, -12, 124, 74). Margin box: 16px 0 — a 2-value shorthand, vertical 16, horizontal 0 — adds 16 to top and bottom only → Rect(-12, -28, 124, 106), width unchanged from the border box, height up by 32.

Where This Connects

This chapter's findingWhat it connects to
One expand_rect_by function driving all three outer boxesChapter 3's own block layout, which will use margin_box()/border_box() directly to decide how much vertical space a block actually occupies and where the next sibling starts
A genuinely real bug (bare unitless zero) traced directly to already-published Chapter 9 codeChapter 10's own capstone integration bug, from Course 1 — another case where a value produced correctly by an earlier chapter exposed a genuine gap only once a later chapter actually tried to consume it
CSS shorthand's own real, specific value-count rulesA recurring theme across this whole site's CSS-adjacent material: shorthand properties look simple but encode real, easy-to-misremember rules — getting the 3-value case backwards is a mistake real CSS authors make too, not just an engine bug

Hands-On Exercises

Exercise 1

Call expand_shorthand() on "4px 8px 12px 16px" (a full 4-value form), "3px 6px" (2-value), and "20px" (1-value). Confirm each produces the exact EdgeSizes real CSS's own shorthand rules predict, and state which one of the three forms is the only one where all four sides can end up genuinely different from each other.

📄 View solution
Exercise 2

Build a Dimensions for a content box 200×100 at the origin, with padding: '4px 8px 12px 16px', border-width: '3px 6px', and margin: '20px'. Compute padding_box() and border_box() and confirm both against hand-worked arithmetic, showing your work for each edge separately rather than only checking the final numbers.

📄 View solution
Exercise 3

Reproduce this chapter's own two bugs directly: call parse_length_naive('0') and confirm it raises ValueError, then call expand_shorthand_naive("10px 20px 30px") and confirm its own left value is wrong. For each bug, identify the exact line responsible and explain, in your own words, why the mistake is easy to make even though it's wrong.

📄 View solution

Chapter 2 Quick Reference

  • Four boxes: content → padding → border → margin, each one the box before it grown outward by a real EdgeSizes
  • Scope: box-sizing: content-box only — width/height describe the content box; border-box is out of scope
  • Real bug found and fixed: a naive length parser assuming every value ends in 'px' crashes on CSS's own valid bare zero — already present in Chapter 9's own published UA stylesheet (margin: 16px 0)
  • Real bug found and fixed: naive 3-value shorthand expansion reuses the top value for left instead of the correct horizontal (second) value
  • Verified: all four shorthand forms (1/2/3/4 values) match real CSS's own defined expansion rules exactly
  • Next chapter: Block Layout — using these box dimensions to actually stack a block box's own children vertically and compute real widths and heights