Constraint-Based Width: The "Auto" Value Puzzle

Building a Web Browser Engine: Layout & Rendering

Chapter 4 · Constraint-Based Width: The "Auto" Value Puzzle

Chapter 3 handled exactly one case: width: auto, with margins that were never anything but a plain number. Real CSS allows three independent things to be auto at once — width, margin-left, and margin-right — and which one (or two) actually end up auto changes which equation the browser has to solve. This chapter builds the real algorithm.

Scope note
This chapter implements the algorithm for left-to-right (LTR) content only, matching this course's own stated scope — right-to-left layout swaps which margin absorbs the over-constrained case, and isn't covered here.

A Bug Inherited Directly From Chapter 3's Own Approach

Chapter 3 computed margin the same way it computed padding and border-width — expand_shorthand(), straight from Chapter 2. That function calls parse_length() on every token in a shorthand value, unconditionally.

Verified directly — the single most famous CSS idiom in existence crashes this engine outright
margin: 0 auto — the classic "center this block horizontally" trick, quite possibly the most-copy-pasted line of CSS ever written — fed through Chapter 3's own margin handling raises ValueError: unsupported length: 'auto'. expand_shorthand() was built for padding and border-width, two properties that genuinely never allow auto at all — parse_length('auto') has no case for that string, and never needed one, until margin (which does allow auto) started reusing the same function.
class MarginEdges: """Like EdgeSizes, but each field may hold the STRING 'auto' instead of a number -- margin is the only one of the three that allows it.""" def __init__(self, top, right, bottom, left): self.top = top; self.right = right; self.bottom = bottom; self.left = left def parse_margin_component(s): s = s.strip() if s == 'auto': return 'auto' return parse_length(s) def expand_margin_shorthand(value_str): parts = [parse_margin_component(p) for p in value_str.split()] # same 1/2/3/4-value expansion as Chapter 2's own expand_shorthand, # just using parse_margin_component instead of parse_length ... return MarginEdges(t, r, b, l)
Verified directly — 'auto' is now a real, distinct value, not a crash
expand_margin_shorthand('0 auto') returns top=0.0, bottom=0.0, left='auto', right='auto' — the 2-value shorthand rule (vertical, horizontal) applying exactly as before, just with the horizontal component preserved as the literal string 'auto' rather than an attempted (and failing) number parse.

The Real Algorithm: Three Cases

margin-left + border-left + padding-left + width + padding-right + border-right + margin-right has to equal the containing block's own width. Solve for whichever piece(s) are auto.

Verified directly — width:auto still matches Chapter 3's own already-verified result exactly
A 300px container, padding: 10px, no margin set (defaults to a plain, non-auto 0). Content width: 280.0 — identical to Chapter 3's own result. This chapter's algorithm is a strict generalization, not a replacement.
Verified directly — a real, genuinely surprising CSS rule: an explicit margin-right can be silently overridden
width: 100px; margin: 20px; (both sides, explicitly 20px each) in that same 300px container. Final margin.left: 20.0, exactly as declared. Final margin.right: 180.0not the declared 20px. This isn't a bug in this engine; it's a real, specified rule: when width and both margins are all non-auto, the total is over-constrained, and (in LTR) the browser silently recomputes margin-right to make everything fit exactly, discarding whatever value was actually written for it. Real CSS authors get caught out by this regularly.
Verified directly — margin:0 auto genuinely centers the box, numerically
Same container, width: 100px; margin: 0 auto;. Final margin.left: 100.0. Final margin.right: 100.0. 100 + 100 + 100 = 300 — the box sits exactly centered, because both margins being auto means the leftover space (containing block width − everything else) is split evenly between them.

Where This Connects

This chapter's findingWhat it connects to
A dedicated MarginEdges/expand_margin_shorthand, distinct from Chapter 2's own EdgeSizes/expand_shorthandChapter 2's own established pattern of one shared mechanism (expand_rect_by) driving all three outer boxes — this chapter shows that pattern has a real limit: margin's own auto behavior is genuinely different from padding/border-width, and reusing identical code for all three was a mistake worth catching explicitly
The over-constrained case silently discarding a declared margin-right valueA real, well-known CSS gotcha independent of this course entirely — worth knowing when debugging a real page where an author's own margin value seems to be "ignored" by the browser for no apparent reason
Both-auto margins splitting the leftover space evenlyChapter 3's own calculate_block_position, unchanged — once dimensions.margin.left holds a real, resolved number (never the string 'auto'), position calculation works exactly as already built, with no changes needed there at all

Hands-On Exercises

Exercise 1

Build a box with width: 100px; margin: 0 20px 0 auto; — a 4-value shorthand where only left is auto — inside a 300px containing block. Compute margin.left and margin.right and confirm the explicitly-set margin-right (20px) is left completely untouched while margin-left alone absorbs the entire remaining space.

📄 View solution
Exercise 2

Build a box with width: 250px; margin: 10px auto; inside a 400px containing block. Confirm the box centers correctly (both horizontal margins equal, and their sum plus the width equals the container's own width), and confirm the shorthand's own vertical component (10px) is completely unaffected by the auto-margin solving.

📄 View solution
Exercise 3

Reproduce this chapter's own crash directly: call calculate_block_width_naive() on a box with margin: 0 auto and confirm it raises ValueError. Identify the exact function call inside calculate_block_width_naive responsible, and explain specifically why padding and border-width never trigger this same crash even though they go through structurally identical shorthand-parsing code.

📄 View solution

Chapter 4 Quick Reference

  • Real bug found and fixed: reusing Chapter 2's own general-purpose shorthand expander for margin crashes on auto — margin needs its own MarginEdges/expand_margin_shorthand, since it's the only one of the three (margin/border/padding) that CSS allows auto on at all
  • width:auto (margins fixed) — width absorbs all remaining space, exactly matching Chapter 3's own already-verified behavior
  • Neither margin auto, explicit width — over-constrained (or an exact fit): margin-right is silently recomputed to fill the leftover space, its own declared value discarded (a real, specified CSS rule, not a bug)
  • One margin auto — that margin alone absorbs all remaining space
  • Both margins auto — the classic centering idiom: leftover space is split evenly between left and right, verified numerically centered
  • Next chapter: Inline Layout & Line Boxes — giving real width and height to the text and inline content this course has deferred since Chapter 1