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.

What you are protecting against How the layer method fixes it What the old way did instead
Specificity war: every new rule needs a longer selector Layer priority beats specificity; (0,1,0) in a higher layer beats (0,3,0) in a lower layer Added #id or !important to win, making the next rule need even more
Dead CSS nobody can prove safe to delete Coverage audit identifies unreachable rules; layers let you keep the uncertain ones isolated Guesswork, or a purge that deletes live styles for a missing state
A rename nobody dares do New layered styles override legacy in the same layer order; you can rename a class and ship it with a fallback
Third-party styles fighting your overrides Wrap the library in a layer or place your layers after it A specificity arms race against the vendor’s selector depth
Source order dependency: move a rule and the page breaks Layer order is declared once, so source order within a layer no longer matters You reordered rules by hand and prayed

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 ?wzxhzdk:5?box-sizing?wzxhzdk:6? in the computed styles: it will say ?wzxhzdk:7?content-box?wzxhzdk:8? or ?wzxhzdk:9?border-box?wzxhzdk:10?, and that one word explains the discrepancy. The DevTools also show the margin collapse in action: the computed ?wzxhzdk:11?margin-top?wzxhzdk:12? 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 ?wzxhzdk:13?min-content?wzxhzdk:14? or ?wzxhzdk:15?max-content?wzxhzdk:16? 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?wzxhzdk:5? 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 ?wzxhzdk:6?box-sizing: content-box?wzxhzdk:7? and a padding will still overflow its track by that padding, unless the track is sized with ?wzxhzdk:8?fr?wzxhzdk:9? units that account for it.

Understanding the box model is what prevents layout bugs in both. The classic bug is a flex item with ?wzxhzdk:10?width: 100%?wzxhzdk:11? inside a flex container with ?wzxhzdk:12?gap: 1rem?wzxhzdk:13?. The gap is applied as a margin-like separation between items, and the ?wzxhzdk:14?width: 100%?wzxhzdk:15? does not include that gap, so the item overflows the container. The fix is to use ?wzxhzdk:16?flex-basis: 0?wzxhzdk:17? and ?wzxhzdk:18?flex-grow: 1?wzxhzdk:19? instead of a percentage width, or to use the gap-aware sizing that flexbox provides. Grid has the same issue with ?wzxhzdk:20?1fr?wzxhzdk:21? tracks: a track sized ?wzxhzdk:22?1fr?wzxhzdk:23? 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 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 ?wzxhzdk:3?box-sizing?wzxhzdk:4?: if it is ?wzxhzdk:5?content-box?wzxhzdk:6?, 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 ?wzxhzdk:7?auto?wzxhzdk:8?, which you fix by adding an explicit height to the containing block or by switching the parent to ?wzxhzdk:9?display: flex?wzxhzdk:10?.

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

The table below compares the two

Scope And Design Tokens Complete The Architecture

box-sizing

The modern CSS architecture migration is not just about layers. It is also about scope. @scope lets you limit a selector's reach to a DOM subtree without increasing specificity. This is the tool for component isolation. It answers the question of why your card styles leak into the footer. The legacy way was to write a selector with enough weight to win everywhere but hope it did not match anything outside the component. The modern way is to declare a scope root and let the browser enforce the boundary. The failure mode with @scope is using it to increase specificity instead of to limit reach, which recreates the specificity war under a new name. Use @scope to say "these styles apply inside this subtree", not to say "these styles beat everything."

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

Another piece of the migration is design tokens. Custom properties are the mechanism. They beat preprocessor variables because they inherit, cascade, and update at runtime. A preprocessor variable is baked at build time; a custom property is resolved at computed value time. That distinction matters for theming. If you want a dark mode toggle, you cannot do it with a Sass variable that was compiled in 2024. You can do it with a custom property that flips on the html element. The migration path: replace hardcoded color values with custom properties, one at a time, and register them with @property so they get type safety. The mistake with @property is declaring a syntax that does not match the initial value, which makes the property invalid at parse time and silently breaks everything that uses it.

border-box’, ’width: 50%?wzxhzdk:5? means half the containing block, including your padding and border, so two columns side by side with padding fit exactly. With ?wzxhzdk:6?content-box?wzxhzdk:7?, ?wzxhzdk:8?width: 50%?wzxhzdk:9? 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.

Ensure A Graceful Fallback For Older Browsers

The question of whether cascade layers are safe to use in production is answered by the @supports fallback. Browsers that do not support @layer will ignore the @layer statement and parse the rules inside it as unlayered styles. The fallback is automatic: the legacy styles apply normally, the new layered styles apply normally, and there is no layering, so the specificity war continues in those old browsers. The fallback pattern is @supports not (@layer) { /* unlayered styles */ }, but you rarely need it because the layer syntax is designed to degrade gracefully. The more important check: do not accidentally put unlayered styles outside any @layer block. Those will have the highest priority and defeat the purpose of layering.

The Honest Caveat

The box model declarations you write have a real cost in layout, paint, and composite. Changing an element?klzzwxh:0000? 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 ?wzxhzdk:1?transform?wzxhzdk:2? and ?wzxhzdk:3?opacity?wzxhzdk:4? 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 ?wzxhzdk:5?offsetWidth?wzxhzdk:6? and then writes ?wzxhzdk:7?width?wzxhzdk:8? 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 ?wzxhzdk:9?transform?wzxhzdk:10? 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.

More in this guide

CSS Layouts in the Fewest Declarations That Actually Work Across EnginesCSS Centering Guide: The Fewest Declarations for Horizontal and Vertical Centering.