Tags: web-dev concept

The Box Model

Date: 2026-09-27


Every element is a rectangle of content, padding, border and margin, and width means the content box unless you say otherwise. Set box-sizing: border-box everywhere, then learn the three places the model still surprises: margin collapse, percentage padding, and inline boxes.


The box model is how CSS sizes an element: a content area, surrounded by padding, then border, then margin, where each layer adds to the space the element occupies. box-sizing decides which of those layers width and height refer to.

content-box vs border-box

.box { width: 300px; padding: 20px; border: 5px solid; }

content-box (the default)                border-box
┌─ border 5 ────────────────────┐        ┌─ border 5 ──────────────┐
│┌─ padding 20 ───────────────┐ │        │┌─ padding 20 ─────────┐ │
││  content 300               │ │        ││  content 250         │ │
│└────────────────────────────┘ │        │└──────────────────────┘ │
└───────────────────────────────┘        └─────────────────────────┘
rendered width = 300+40+10 = 350         rendered width = 300

content-box means adding padding changes the layout. Two 50%-wide columns with any padding overflow their row. border-box makes width mean what a designer means by it, which is why every reset starts with it:

*, *::before, *::after { box-sizing: border-box; }

Margin collapse

Vertical margins between blocks don’t add — the larger one wins.

<h2 style="margin-bottom: 32px">          expected gap: 32 + 24 = 56
<p  style="margin-top: 24px">             actual gap:   32  ← collapsed

It also happens between a parent and its first or last child. A child’s margin-top pokes out of the parent, and the gap shows up above the parent — the classic “why is my header pushed down” bug.

It only happens in normal block flow. It stops when:

  • the parent is a flex or grid container — children’s margins never collapse
  • padding or border separates the two margins
  • the parent creates a new block formatting context (an independent layout region — display: flow-root, overflow other than visible)

That’s why the bug often “fixes itself” when a layout moves to flex, and why many systems use gap or one-direction margins (only ever margin-block-end) so collapse never comes into play — Spacing Systems.

Percentages resolve against width, even vertically

padding-top: 50% is half the containing block’s width, not its height. That was the basis of the old aspect-ratio hack (padding-top: 56.25% for 16:9). aspect-ratio: 16 / 9 replaces it, and the hack is worth recognising in legacy code rather than writing.

Inline boxes are different

A display: inline element (span, a, em) ignores width and height, and its vertical padding and margin don’t push surrounding lines apart — padding paints, but can overlap the lines above. That’s why an <a> given padding to make a larger tap target looks right and overlaps its neighbours. inline-block or inline-flex gives it a real box.

Failure modes

  • Overflow from content-box in any codebase missing the reset, or a third-party widget that assumes the other model
  • Spacing that differs between contexts because margins collapse in block flow and not in flex. The same component gets different gaps in a sidebar and in a grid
  • Negative margins to cancel margin collapse, which then break when the parent’s layout mode changes
  • width: 100% plus margin overflows whatever box-sizing says — margin is always outside. A block element in normal flow already fills the width by default, so the fix is usually to delete width: 100%

Where it connects

  • Flexbox · Grid — size boxes with their own rules on top of this model
  • Logical Properties — margin-block, padding-inline: the same box, named by writing direction