The CSS Box Model: Margin, Border, Padding and box-sizing in Modern Layouts

Explains the CSS box model's actual dimension calculation rules, margin collapse, and why box-sizing: border-box replaced the 2014 width-calculation hacks.

Your layout is 20 pixels too wide, and you have already burned an hour subtracting borders from widths in your head. The answer is one declaration: box-sizing: border-box on everything. That single line replaces the 2014 hacks named below, and it turns the box model from a subtraction problem into a measurement you can trust. Here is the complete calculation in the fewest declarations that actually work, then the failure cases that still bite.

The Whole Calculation in Four Lines

The box model has four nested boxes: the content area, the padding wrapper, the border, and the outer margin. The content area holds your text and images. Padding wraps it. The border adds a stroke around the padding. Margin is the outer space that separates this piece from its neighbours. In the default box-sizing: content-box, the width you write sets only the content area. The used width in the layout is that content width plus padding-left and padding-right plus border-left-width and border-right-width. Margin is extra, outside the border, and it does not count toward the element’s width at all.

Here is the default calculation, exactly as the browser performs it:

/* Default: box-sizing: content-box */
.element {
  width: 300px;
  padding: 20px;
  border: 5px solid black;
  margin: 10px;
  /* Total width = 300 + 20 + 20 + 5 + 5 = 350px */
  /* Plus margin = 370px of horizontal space occupied */
}

That 350px is what pushes your grid column over. The fix is box-sizing: border-box, which makes the width property include the padding and border. The content area shrinks to fit inside that declared width. The height behaves the same way, but only when the height is explicit; with height: auto, the content determines the height and padding and border add on top, which is the behaviour you want for most text.

How `box-sizing: border-box` Changes Which Number You Write

With box-sizing: border-box, the width you declare is the border-box width. The browser subtracts the padding and border from that declared width to compute the content area. The margin remains outside, never included in the width property. That is common mistake number one: margin is not inside the border. The percentage sizing rules also change meaning: a width: 50% with border-box means half the containing block’s total width, including your padding and border. Which is what you wanted all along.

Here is the same element with border-box:

.element {
  box-sizing: border-box;
  width: 300px;
  padding: 20px;
  border: 5px solid black;
  margin: 10px;
  /* Total width = 300px (padding and border inside) */
  /* Content box = 300 - 20 - 20 - 5 - 5 = 250px */
  /* Plus margin = 320px of horizontal space occupied */
}

That is the difference. In content-box, you declare the content width and the browser adds padding and border. In border-box, you declare the total element width and the browser subtracts. The modern default is still content-box per the specification, but every browser engine shipped border-box support decades ago: Gecko in Firefox 1.0, WebKit in Safari 1.0, Blink in Chrome 1.0, and EdgeHTML in Edge 12. The 2014 hack was applying it with the universal selector:

/* The 2014 cargo cult: */
* {
  box-sizing: border-box;
}

That universal selector works, but it is a sledgehammer. It applies border-box to every element including replaced elements like images and form controls where you may not want it, and it creates a specificity war if you ever need to opt out. The modern replacement is to set it on the root and let inheritance not apply, because box-sizing is not an inherited property, so you still need the universal selector or a reset that includes it. The honest answer is that the universal selector is not wrong, it is blunt. The 2014 version you found on StackOverflow also included the padding-percentage-for-aspect-ratio trick, which this guide replaces entirely.

The Modern Default: What It Replaces

The padding-percentage-for-aspect-ratio trick was the ugliest hack in the 2014 toolkit. Because percentage padding and margin (top and bottom) reference the parent’s inline size, not its block size, you could set padding-top: 56.25% on a zero-height element to create a 16:9 box. That worked, but it required a wrapper, an absolutely positioned child, and a magic number for every aspect ratio you needed. Modern CSS has aspect-ratio, which does the same job with one declaration and no percentage trickery:

.video-container {
  aspect-ratio: 16 / 9;
  width: 100%;
  /* height auto, the aspect ratio determines the block size */
}

That is the replacement. The aspect-ratio property works with any width and computes the height from the ratio, and it respects min-content and max-content constraints. The other 2014 hack this guide replaces is the overflow-hidden clearfix. That was a float-clearing technique: you added overflow: hidden to a parent to contain floated children, which created a new block formatting context and forced the parent to include them. The clearfix died when flexbox and grid became the layout tools, because neither uses floats, and overflow: hidden for clearing is now a bug source: it clips content and creates a scroll container unexpectedly. Understanding the box model underpins both flexbox and grid, because both still calculate widths and heights from the same four boxes, they just distribute space differently.

Margin Collapse Rules: The One That Still Surprises

Margin collapse is the box model behaviour that makes adjacent vertical margins not add up. Two block-level siblings in the normal flow each have margin-top: 20px and margin-bottom: 20px. The space between them is not 40px. It is 20px, because the larger of the two adjacent margins wins and the smaller collapses into it. The rule applies to vertical margins (the block direction) only, never to horizontal margins (the inline direction). The collapse also happens between parent and first or last child: if a parent has no border or padding in the collapsing direction, the child’s margin can collapse through and push the parent instead.

The exceptions are where modern layout saves you. Margin collapse does not happen for flex items, grid items, absolutely positioned elements, or elements with overflow other than visible. That is why a flex container with gap does not collapse: the gap is not a margin, it is a separate distribution mechanism. Here is the collapse in action:

.container {
  /* no border, no padding, no overflow set */
}
.item {
  margin-top: 30px;
  margin-bottom: 20px;
}
.first-child {
  margin-top: 40px;
}
/* The gap between .first-child and .item is 40px, not 70px */

The margin collapse rules exist because the original CSS authors wanted vertical whitespace to feel consistent in prose-like documents. In a card grid or a toolbar, you do not want that behaviour, so you use flex or grid, which establish a flex formatting context or a grid formatting context that disables the collapse entirely. The failure case is when you debug a gap that is smaller than you wrote: check for margin collapse before you check for a typo.

Padding and Border Layout: Where the Arithmetic Goes Wrong

The padding and border layout calculation is where most layout bugs live, because percentage sizing and intrinsic size interact with the box model in non-obvious ways. Percentage padding and percentage margin always reference the parent’s inline size, which is the width in a horizontal writing mode, even for the top and bottom values. That is the rule that made the aspect-ratio hack possible, and it is the rule that breaks when you expect padding-top: 10% to mean ten percent of the parent’s height. It does not. It means ten percent of the parent’s width.

Percentage width references the containing block’s width, and percentage height references the containing block’s computed height, which is auto by default. That is why percentage height collapses to zero: the parent has no explicit height, so the percentage resolves against auto, which means the child gets height: auto, which means it sizes to its content. The fix is to give the parent an explicit height or to use a flex or grid container, which has a definite height in its main axis.

The border contributes to the border box, and with border-box, the border eats into the content area. A common mistake is forgetting that box-sizing: border-box does not include margin in the width or height calculation, so a grid item with width: 100% and margin-left: 20px will overflow its track by 20px. The margin is always outside the border, and no box-sizing value changes that. The DevTools inspection panel shows this clearly: it overlays the content area, padding, and border in different colours, and it lists the used value of each. Open it on a broken element and read the numbers.

DevTools Inspection: Read the Numbers, Not the Guess

The DevTools inspection is the single most reliable way to debug a dimension problem. In Chrome, Firefox, or Safari, select an element and the Styles panel shows a box diagram with four nested rectangles. Hover over the inner content area to see its pixel dimensions, over the padding for the padding values, over the border for the border widths, and over the margin for the margin values. The computed styles tab shows the used value of width and height, which is the number the layout engine actually applied, after every border-box calculation and every percentage resolution.

When the layout is wrong, do not reason from the CSS you wrote. Reason from the computed values. If the width you declared is 300px and the computed width is smaller, something is subtracting pixels, likely a padding or a border that box-sizing does not cover. Inspect the element’s box-sizing in the computed styles: it will say content-box or border-box, and that one word explains the discrepancy. The DevTools also show the margin collapse in action: the computed margin-top of the second sibling will be the collapsed value, not the declared one.

The failure case is when the DevTools show the number you expect and the layout is still wrong. That means the problem is not the box model of that element; it is the containing block. Check the parent’s size, check whether the parent is a flex container or grid container that distributes space differently, and check whether an ancestor has a width constraint like min-content or max-content that is forcing a shrink-wrap behaviour. The box model is local, but the containing block is global.

The box model underpins both flexbox and grid because every flex item and every grid item is still a box with content, padding, border, and margin. Flexbox distributes space along one axis, the main axis, and it uses the flex-basis, flex-grow, and flex-shrink properties to resolve how the free space is allocated among items. Grid distributes space along two axes, the inline axis and the block axis, with explicit row and column tracks. Neither layout system changes the box model; they change how the boxes are positioned and how the free space is divided. The flex and grid formatting contexts disable margin collapse, but they do not change the padding and border calculation. A grid item with box-sizing: content-box and a padding will still overflow its track by that padding, unless the track is sized with fr units that account for it.

Understanding the box model is what prevents layout bugs in both. The classic bug is a flex item with width: 100% inside a flex container with gap: 1rem. The gap is applied as a margin-like separation between items, and the width: 100% does not include that gap, so the item overflows the container. The fix is to use flex-basis: 0 and flex-grow: 1 instead of a percentage width, or to use the gap-aware sizing that flexbox provides. Grid has the same issue with 1fr tracks: a track sized 1fr plus a gap can push the total beyond the container width if the container itself has padding. The box model is the layer below the layout system, and the layout system assumes you know how it works.

The Failure Case: When the Normal Route Is Closed

The normal route to a working layout is box-sizing: border-box on everything and a modern layout system. When that route is closed, you are debugging at 1am and the DevTools show a number you did not write. The first action is to check the computed box-sizing: if it is content-box, that is the bug. The second action is to check for margin collapse by setting a border or padding on the parent in the collapsing direction. The third action is to check for a percentage height that resolved against auto, which you fix by adding an explicit height to the containing block or by switching the parent to display: flex.

The universal selector reset is the fallback when the framework you are using sets box-sizing inconsistently. It is not elegant, but it is deterministic: * { box-sizing: border-box; } applies to every element, and you can override it with a more specific rule for the elements that need content-box, like a replaced element that must keep its intrinsic size. The replaced elements are the exception: images and videos have an intrinsic size, and border-box on an image with width: 100% and a padding will shrink the image’s content area, which is usually not what you want. For those, use content-box or set the padding to zero.

The table below compares the two box-sizing values across the three axes that matter for layout: what the width property controls, what the height property controls, and how percentage sizing resolves. The third column is the practical answer for which one to use in a modern codebase.

Box size mode Width property controls Height property controls Percentage sizing resolves against Recommendation
content-box (default) Content width only Content height only Containing block’s content box Use only for replaced elements needing intrinsic size
border-box Content + padding + border Content + padding + border Containing block’s border box Use for everything else

The difference is one declaration, but it changes every width you write from a guess into a contract. With border-box, width: 50% means half the containing block, including your padding and border, so two columns side by side with padding fit exactly. With content-box, width: 50% means the content is half, and the padding and border push the element beyond the half, creating the overflow that the 2014 hacks existed to fix.

aspect-ratio is the modern replacement for the padding hack, but it has a constraint: it only works when one dimension is auto. If you set both width and height, the aspect-ratio is ignored and the explicit dimensions win, which is the correct behaviour for a media element where the user resized the window. The intrinsic size of a replaced element, its natural width and height in pixels, still governs when no explicit dimension is set, and aspect-ratio does not override that. For a video element with a 16:9 source, the browser uses the intrinsic ratio automatically; you only need aspect-ratio when the element has no intrinsic ratio, like a div or a canvas.

The formatting context is the other term that changes how the box model behaves. A block formatting context is created by the root element, by floats, by overflow: hidden, and by display: flow-root. Inside a block formatting context, margins collapse and the normal flow rules apply. A flex formatting context is created by display: flex, and it disables margin collapse, changes the block direction to the flex direction, and makes children flex items. A grid formatting context is created by display: grid, and it disables margin collapse as well. Every element is in exactly one formatting context, and that context determines which box model rules apply. When you debug a margin collapse that should not happen, check which formatting context the element is actually in: a parent with display: block creates a block formatting context, and the collapse is legal.

The Cost of Box Model Declarations in Layout and Paint

The box model declarations you write have a real cost in layout, paint, and composite. Changing an element’s width or height triggers layout for that element and all of its descendants, and it reflows siblings in the same formatting context. Changing padding or border also triggers layout, because the content area resizes. Changing margin triggers layout for the element and its siblings, because the space between them changes. The compositor only avoids layout for transform and opacity changes; any box model property change goes through the full layout pipeline.

The performance-conscious developer does not avoid box model changes; they avoid changing them in a loop. A JavaScript animation that reads offsetWidth and then writes width in the same frame forces the browser to recalculate layout twice, a pattern called layout thrashing. The fix is to batch reads and writes, or better, to use CSS animations that the browser can run on the compositor. For a static page, the cost of box model declarations is paid once at initial layout, and the browser is fast enough that you do not need micro-optimizations. The cost matters when you dynamically add or remove elements, when you change a class that toggles a width, or when you animate a box model property. In those cases, prefer transform for the animation and keep the box model static.

The containing block is the reference frame for percentage sizing and for absolute positioning. For a static element, the containing block is the content box of its parent. For an absolutely positioned element, the containing block is the padding box of the nearest ancestor that has position: relative, absolute, fixed, or sticky. That distinction is why a percentage width on an absolutely positioned child resolves against the padding box, not the content box, of its positioned ancestor. The containing block for a fixed element is the viewport, which is the initial containing block.

The margin collapse rules and the containing block rules are both part of the box model specification. They are stable, they have not changed since the 1990s, and they are the reason why the 2014 hacks were needed: the early CSS authors did not anticipate the card-based layouts that modern web design uses, and the box model defaults punished that design. The modern answer is not to fight the box model; it is to use box-sizing: border-box and a layout system that disables the troublesome collapse behaviour.

Frequently Asked Questions

Does box-sizing: border-box include margin in the width? No. The margin is always outside the border. The width property with border-box includes content, padding, and border, but not margin. To include margin in the total space, use the margin shorthand separately and calculate the total as width plus margin.

Why does margin collapse happen and how do I stop it? Margin collapse is a specification rule for the normal flow: adjacent vertical margins collapse to the larger value. To stop it, create a new formatting context on the parent with display: flow-root or overflow: hidden, or switch to a flex or grid container, which disables collapse.

What is the difference between content-box and border-box? With content-box, the width property sets the content width, and padding and border add to it. With border-box, the width property sets the total width including padding and border, and the content box shrinks to fit. The default is content-box.

How do I use percentage padding correctly? Percentage padding always references the parent's inline size, which is the width in horizontal writing modes, even for top and bottom. It never references the parent's height. For a vertical padding that matches a percentage of the height, use a fixed length or a CSS custom property.

What is the modern replacement for the clearfix hack? The clearfix hack set overflow: hidden to contain floats. The modern replacement is to use display: flow-root on the parent, which creates a block formatting context without clipping overflow, or to use flexbox or grid, which do not use floats at all.