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.
When I set width: 300px, what exactly is 300 pixels wide, and why does the element occupy more space than that?
Someone wants three cards side by side in a row: comfortable inner padding, a hairline border, even gaps. They expect the arithmetic to work.
A card is width: 300px, padding: 16px, border: 1px solid. Three of them fit in a 900px container, because 3 × 300 = 900.
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.
- 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%pluspadding: 16pxon 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
backgroundandmargin: 16pxhas 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-ratioresponsive-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, andautois not definite.
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) meanswidthis the content box and padding plus border are added outside it.border-boxmeanswidthis 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
overflowclipping and no hit testing.box-sizingnever includes it, in either mode. - Percentages on padding and margin resolve against the containing block's inline size, in both axes. Percentages on
widthresolve against inline size too; percentages onheightresolve 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: noneelements 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-sizingchanges the arithmetic, not the cost. Both modes are a subtraction; neither is measurably faster than the other.- Changing
outlineorbox-shadowcosts paint but not layout, because neither participates in geometry. That is the whole reason the focus ring is drawn withoutline.
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.
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
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.
| Read | Rectangle | Includes scrollbar? | Transforms? | Ask it when |
|---|---|---|---|---|
getBoundingClientRect() | Border box, fractional, viewport-relative | Yes (part of the border box) | Applied | You need the box as the user sees it, including sub-pixel position |
offsetWidth / offsetHeight | Border box, rounded to an integer | Yes | Ignored | You need untransformed layout size and can tolerate rounding |
clientWidth / clientHeight | Padding box minus scrollbar | No — excluded | Ignored | You need the space content can actually occupy inside a scroll container |
scrollWidth / scrollHeight | Full scrollable content extent | n/a | Ignored | You are asking whether content overflows, or how far it can scroll |
getComputedStyle(el).width | Used value of width, per box-sizing | Depends on the mode | Ignored | You 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.
| Change | style | layout | paint | composite | Why |
|---|---|---|---|---|---|
| `width`, `padding`, `border-width`, `margin` | yes | yes | yes | yes | Geometry changes, so every box whose position depends on this one must be recomputed, then repainted in its new place. |
| `box-sizing` | yes | yes | yes | yes | It changes what width resolves to, so it is a geometry change wearing a different name. |
| `border-color` | yes | no | yes | yes | Same box, different pixels. Nothing moves, so layout is skipped entirely. |
| `outline`, `box-shadow` | yes | no | yes | yes | Painted outside the border box and excluded from geometry by design — this is exactly why focus rings do not reflow the page. |
| `transform: scale()` | yes | no | maybe | yes | The 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 dimensions | yes | yes | yes | yes | It 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-boxon 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
gapfor spacing between siblings in flex and grid rather than margins on the children.gapis 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-startis correct in both directions;padding-leftis correct in one (Internationalization). - Reserve space for anything that arrives later — images, embeds, iframes — with
aspect-ratioor 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
gapon 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 withoverflow: hiddenand no room will clip it, so a keyboard user sees nothing at all. Leave room, or useoutline-offsetinward. - 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-heightbeatsheightfor anything containing words.
What can go wrong
width: 100%plus horizontal padding undercontent-box: reliable overflow, usually discovered only on the narrowest viewport.- A global
* { box-sizing: border-box }reset applied to a third-party widget that assumedcontent-box, silently shrinking its content area. Scope the reset, or accept that embedded widgets need their own boundary. - Vertical spacing implemented as
margin-topon 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
offsetWidthon 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.
- A web font arriving after first paint changes every text box's intrinsic size, so measurements taken before
document.fonts.readydescribe 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.
- 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.
- "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-boxincludes 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-rationow 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
- 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.offsetWidthgives the border box as a rounded integer, before transforms.clientWidthgives the padding box minus scrollbar.scrollWidthgives 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).
- 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,remorchscales and everything sized inpxdoes 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.
border-boxeverywhere 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-ratioprevents 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-gutterexists to stabilise.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — 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.