LayoutGENERALPLATFORM-SPECIFIC

The Box Model

Every element is four nested edges — content, padding, border, margin — plus a sizing mode that decides which of them width is actually describing.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

When I set width: 300px, what exactly is 300 pixels wide, and why does the element occupy more space than that?

The user intent

Someone wants three cards side by side in a row: comfortable inner padding, a hairline border, even gaps. They expect the arithmetic to work.

The obvious build

A card is width: 300px, padding: 16px, border: 1px solid. Three of them fit in a 900px container, because 3 × 300 = 900.

Why it breaks

Under the default box-sizing: content-box, that card is 300 + 32 + 2 = 334px wide. Three of them need 1,002px, so the third one wraps or overflows, and the fix people reach for — subtracting the padding from the width by hand — breaks the moment the padding changes.

How it breaks in a real browser
  • Under the default box-sizing: content-box, that card is 300 + 32 + 2 = 334px wide. Three of them need 1,002px, so the third one wraps or overflows, and the fix people reach for — subtracting the padding from the width by hand — breaks the moment the padding changes.
  • width: 100% plus padding: 16px on the same element overflows its parent by 32px, which is the single most common CSS bug there is.
  • Margins do not paint. A card with background and margin: 16px has no background in the margin area, so a design that expected a 16px band of card colour gets 16px of whatever is behind it.
  • Percentage padding resolves against the containing block's inline size — even padding-top. padding-top: 50% on a 400px-wide box is 200px tall, not half its height, which is why the pre-aspect-ratio responsive-video hack worked at all.
  • height: 100% on a child usually does nothing, because a percentage block size needs a definite block size on the containing block, and auto is not definite.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • Layout generates a box for each element that produces one. The box has four rectangles nested inside each other: the content box, the padding box, the border box, and the margin box.
  • `box-sizing` selects which rectangle `width` and `height` describe. content-box (the initial value) means width is the content box and padding plus border are added outside it. border-box means width is the border box and padding plus border eat into the content.
  • Margin is outside the box entirely. It is space the box reserves in its parent, not part of the box: it takes no background, no border, no overflow clipping and no hit testing. box-sizing never includes it, in either mode.
  • Percentages on padding and margin resolve against the containing block's inline size, in both axes. Percentages on width resolve against inline size too; percentages on height resolve against the containing block's block size only if that size is definite.
  • `outline` and `box-shadow` are painted outside the border box and occupy no layout space. That is deliberate — a focus ring must not reflow the page — and it is why an outline can be clipped by an ancestor's overflow: hidden (Positioning and Stacking Contexts).
  • In a scroll container, a classic (non-overlay) scrollbar is taken out of the content box, so the usable inline size is smaller than the border box implies (Normal Flow, Overflow and Margin Collapsing).
  • Logical properties — padding-inline, margin-block, inline-size — name the edges by writing direction rather than by screen direction, which is what makes a layout survive being translated into Arabic or Japanese.

What this makes the browser do

And which of it is avoidable.

  • Layout computes and stores four edges per box. That is per box in the layout tree, not per element in the DOM: display: none elements generate no box, and one element can generate several (The Rendering Pipeline).
  • Changing any geometric property — width, padding, border-width, margin, font-size — dirties the box and forces layout for it, its descendants, and any ancestor or sibling whose own size depends on it (The Cost of a Change).
  • box-sizing changes the arithmetic, not the cost. Both modes are a subtraction; neither is measurably faster than the other.
  • Changing outline or box-shadow costs paint but not layout, because neither participates in geometry. That is the whole reason the focus ring is drawn with outline.

Four edges, and which one `width` means

Draw the box once and most box-model arguments end. The content box holds the text and children. Padding surrounds it and is painted with the element's background. The border surrounds the padding and is painted with its own colour. Margin surrounds the border and is painted with nothing at all — it is reserved space in the parent, not part of the element.

The only genuinely confusing part is that width does not name a fixed rectangle. Under content-box it names the innermost one and everything else is added outside; under border-box it names the third one and padding and border are subtracted from the inside. Both are consistent; the default just happens to be the one that makes arithmetic hard.

The reset that removes the arithmetic
1*, *::before, *::after { box-sizing: border-box; }
2
3.card {
4 inline-size: 300px; /* logical: `width` in horizontal writing modes */
5 padding-inline: 1rem;
6 border: 1px solid;
7 /* border box is exactly 300px. three fit in 900px. */
8}
9
10/* spacing lives on the container, not on the children */
11.row { display: flex; gap: 1rem; }

The gap on the row is the second half of the fix: with margins on the children you get a stray edge at each end and a collapsing question you did not ask for.

margin box  . . . . . . . . . . . . . . . . . . . . . . . . . .
            .                                                   .
            .  border box  +---------------------------------+  .
            .              |  padding box                    |  .
            .              |   +-------------------------+   |  .
            .              |   |                         |   |  .
            .              |   |      content box        |   |  .   margin: 16px
            .              |   |                         |   |  .   border: 1px
            .              |   +-------------------------+   |  .   padding: 16px
            .              |                                 |  .
            .              +---------------------------------+  .
            .                                                   .
            . . . . . . . . . . . . . . . . . . . . . . . . . . .

  box-sizing: content-box     width: 300px  ->  content 300 | border box 334 | margin box 366
  box-sizing: border-box      width: 300px  ->  content 266 | border box 300 | margin box 332

  painted:      background covers content + padding; border paints itself
  not painted:  margin  (no background, no border, no hit testing)
  no layout:    outline, box-shadow  (drawn outside the border box, occupy nothing)

Five ways to ask how wide something is

GENERALThe definitions come from CSSOM View and agree across engines. What differs is what they return during a lazy layout: all five force a synchronous layout if one is pending, and the cost of that flush is engine-specific (Layout Thrashing).

Every measurement API returns a different rectangle, and each one is the right answer to a different question. Reaching for whichever you remember is how a layout ends up a scrollbar-width off on one platform and a rounding error off on another.

One property in this table is not like the others: getBoundingClientRect() is the only one that reports the box as painted, after transforms, in fractional pixels. If an ancestor is scaled, every offset* number describes an element that is no longer that size on screen.

ReadRectangleIncludes scrollbar?Transforms?Ask it when
getBoundingClientRect()Border box, fractional, viewport-relativeYes (part of the border box)AppliedYou need the box as the user sees it, including sub-pixel position
offsetWidth / offsetHeightBorder box, rounded to an integerYesIgnoredYou need untransformed layout size and can tolerate rounding
clientWidth / clientHeightPadding box minus scrollbarNo — excludedIgnoredYou need the space content can actually occupy inside a scroll container
scrollWidth / scrollHeightFull scrollable content extentn/aIgnoredYou are asking whether content overflows, or how far it can scroll
getComputedStyle(el).widthUsed value of width, per box-sizingDepends on the modeIgnoredYou want the resolved CSS value rather than a geometric fact

What each box change costs

Box properties divide cleanly by which pipeline stage they invalidate, and the division is worth memorising because it explains why two visually similar edits have very different costs.

The rule underneath: if a property can change where anything else ends up, it costs layout. If it only changes what is drawn inside a box whose position is already known, it costs paint. If it is handled by the compositor on already-painted content, it costs neither.

Changing a box
ChangestylelayoutpaintcompositeWhy
`width`, `padding`, `border-width`, `margin`yesyesyesyesGeometry changes, so every box whose position depends on this one must be recomputed, then repainted in its new place.
`box-sizing`yesyesyesyesIt changes what width resolves to, so it is a geometry change wearing a different name.
`border-color`yesnoyesyesSame box, different pixels. Nothing moves, so layout is skipped entirely.
`outline`, `box-shadow`yesnoyesyesPainted outside the border box and excluded from geometry by design — this is exactly why focus rings do not reflow the page.
`transform: scale()`yesnomaybeyesThe layout box is untouched; the compositor transforms already-painted content. Repaint only if the element must be re-rasterised at the new scale for sharpness.
`aspect-ratio` on an image with no dimensionsyesyesyesyesIt costs layout once, at parse time, and saves the far worse layout that would have happened when the image decoded (Visual Stability).

caveat Every maybe here depends on the rest of the page: whether the element already has its own compositing layer, whether containment bounds the invalidation, and how much subtree the browser decides is dirty (CSS Containment).

How to build it

Most important first.

  • Set box-sizing: border-box on everything once, at the top of the stylesheet, and stop doing arithmetic. Predictable outer size is worth far more than the rare case where you wanted content-box.
  • Use gap for spacing between siblings in flex and grid rather than margins on the children. gap is owned by the container, does not collapse, and does not add a stray edge to the first or last item (Normal Flow, Overflow and Margin Collapsing).
  • Prefer logical properties for anything a translator will touch. padding-inline-start is correct in both directions; padding-left is correct in one (Internationalization).
  • Reserve space for anything that arrives later — images, embeds, iframes — with aspect-ratio or explicit dimensions, so its arrival does not move content the user is already reading (Visual Stability).
  • Express spacing in one system. Mixing padding on the parent, margin on the child and gap on the container gives three sources of truth for one visual distance and guarantees a fourth will be added later.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • Padding is the hit target; margin is not. A 16px icon with 16px of margin is still a 16px target, and it fails pointer target-size guidance. The same icon with 16px of padding is a 48px target — same pixels on screen, very different to hit.
  • The focus ring is drawn with outline, outside the border box, and takes no space. An ancestor with overflow: hidden and no room will clip it, so a keyboard user sees nothing at all. Leave room, or use outline-offset inward.
  • At 400% zoom, reflow requirements mean content must fit one column without horizontal scrolling. Fixed pixel widths on a card, a table cell or a sidebar are the usual reason a layout fails that: they cannot get narrower (Intrinsic Sizing and the Automatic Minimum).
  • Text spacing overrides — user stylesheets that raise line height, letter spacing and word spacing — must not clip content. Boxes sized to exactly fit today's text will cut it off when a user increases spacing, which is why min-height beats height for anything containing words.

What can go wrong

Failure modes
  • width: 100% plus horizontal padding under content-box: reliable overflow, usually discovered only on the narrowest viewport.
  • A global * { box-sizing: border-box } reset applied to a third-party widget that assumed content-box, silently shrinking its content area. Scope the reset, or accept that embedded widgets need their own boundary.
  • Vertical spacing implemented as margin-top on a child, which then collapses out of the parent and lands somewhere unexpected (Normal Flow, Overflow and Margin Collapsing).
  • A layout built and verified only with overlay scrollbars. On a platform with classic scrollbars the content box is narrower and the layout reflows or overflows.
  • Trusting offsetWidth on an element with a fractional computed size. It is a rounded integer; getBoundingClientRect() is the sub-pixel truth, and the difference shows up as a one-pixel gap at some zoom levels.
What can arrive out of order
  • A web font arriving after first paint changes every text box's intrinsic size, so measurements taken before document.fonts.ready describe a layout that no longer exists (Images and Fonts).
  • An image without intrinsic dimensions is a zero-height box until it decodes, and then abruptly is not. Anything that measured the page in between measured a page the user never saw.
Security
  • Box geometry is readable by any script on the page, and computed sizes vary with fonts, zoom, extensions and platform. That makes layout measurement a fingerprinting surface, which is why the platform has removed several precise measurement paths over time.
  • An attacker who can position a transparent box over your control does not need to change its geometry to be dangerous — hit testing follows the topmost box, and the user aims at what they can see (Clickjacking and Framing).
  • A box whose size is derived from untrusted content is a layout-level denial of service: a single 40,000-character unbroken string can force an enormous max-content size and stall layout (Intrinsic Sizing and the Automatic Minimum).
  • Cross-origin iframes get a box in your layout, but their internal geometry is not readable and yours is not readable by them. The box is the boundary.
Misreads
  • "Margin is part of the element." It is space around the element. It is why a background stops short of where you expected and why hit testing does not extend into it.
  • "box-sizing: border-box includes the margin." It never includes margin, in either mode. The margin box is outside every mode's reach.
  • "padding-top: 50% is half the height." It is half the containing block's inline size. The percentage-padding aspect-ratio hack depended entirely on that surprise; aspect-ratio now expresses the intent directly.
  • "width: 100% means fill the parent." It means "match the containing block's inline size", which is not the same thing once the element itself has padding, borders or a margin.

Measuring it, and what changes in the field

How you would see this
  • The Elements panel's Computed tab draws the box model for the selected element with the resolved numbers for each edge. It answers "which edge is 334px" faster than any reasoning about the cascade.
  • getBoundingClientRect() gives the border box in floating-point CSS pixels, after transforms. offsetWidth gives the border box as a rounded integer, before transforms. clientWidth gives the padding box minus scrollbar. scrollWidth gives the scrollable content extent. Picking the wrong one produces off-by-a-scrollbar bugs.
  • The Layout panel's flex and grid overlays draw the actual boxes rather than the ones you intended, which is usually where the discrepancy becomes obvious (Debugging Rendering and Jank).
Slow device, slow network, large data, old tab
  • With classic scrollbars — Windows, and Linux with most themes — a scroll container loses roughly 15px of inline content size that overlay-scrollbar platforms keep. A three-column layout that fits on macOS can wrap on Windows.
  • With a user font-size preference or page zoom, everything sized in em, rem or ch scales and everything sized in px does not. A mixed layout distorts rather than scaling.
  • With a longer translation, a box sized to today's English string clips or overflows. German runs 30% longer than English for UI strings as a rule of thumb, and that is before considering scripts with taller line boxes.
What this costs
  • border-box everywhere makes outer size predictable at the cost of making content size implicit — you now cannot say "the content must be exactly 300px" without arithmetic in the other direction. That trade is almost always worth taking, but it is a trade.
  • Logical properties are correct in every writing mode and are harder to read for a team that thinks in left and right. The cost is real and one-time; the alternative cost recurs at every localisation.
  • Reserving space with aspect-ratio prevents layout shift and can leave a visible gap when the resource never loads. A gap that stays still is better than content that jumps, but it is not free.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe four edges, the two sizing modes and the inline-size basis for percentage padding are specified in CSS Box Model and behave identically in Blink, Gecko and WebKit — this is one of the genuinely settled parts of the platform.
  • PLATFORM-SPECIFICScrollbar behaviour is not settled: macOS and iOS use overlay scrollbars that take no space from the content box, while Windows and most Linux desktop themes use classic scrollbars that do. The same stylesheet therefore yields a different content width per platform, which is what scrollbar-gutter exists to stabilise.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Software Design — a spacing scale is an interface between design and code; the box model is only the mechanism that renders whichever system you chose.