Ch. 5 · HTML & CSS

CSS Box Sizing and Predictable Dimensions

Why width: 300px can render 342px wide, how border-box changes the arithmetic, and the global reset most projects start with, with numbers.

~5 min readbeginnerupdated Oct 7, 2026

You set width: 300px on a card, add padding: 20px and a 1px border, and the card measures 342px in DevTools. It overflows its 300px column by 42px and the layout breaks. Nothing is wrong with the browser; it’s following the default box model, and the default is not what most people assume.

Where the extra pixels come from

Every element generates a box with four layers, from inside out: content, padding, border, margin. The box-sizing property decides which of those layers width and height describe.

The default is box-sizing: content-box. Under it, width sets the content area only. Padding and border are added outside it:

.card {
  width: 300px;
  padding: 20px;
  border: 1px solid #d4d4d8;
  margin: 16px;
}
CSS

The rendered width of the border box is:

content   300
padding    20 + 20 = 40
border      1 + 1  = 2
           -----
total      342px
Text

The margin adds another 16px each side to the space the card occupies, so it claims 374px of horizontal room, but margin is outside the box and is never counted by box-sizing in either mode. DevTools’ box-model diagram (in the Computed tab in Chrome and Firefox) shows these four layers with their numbers, and it’s the quickest way to see what’s happening.

Now switch the model:

.card {
  box-sizing: border-box;
  width: 300px;
  padding: 20px;
  border: 1px solid #d4d4d8;
}
CSS

With border-box, width describes the border box, so the card renders exactly 300px wide. The browser works out the content area by subtraction: 300 − 40 − 2 = 258px for the text inside. The total stays put, and padding eats into the content instead of pushing the edges outward.

Heights follow the same rule. height: 100px with 20px vertical padding gives a 140px box under content-box and a 100px box (60px of content) under border-box.

Why border-box is easier to reason about

The case that convinces people is a percentage width combined with fixed padding:

.column {
  width: 50%;
  padding: 0 24px;
  float: left; /* or display: inline-block, or a flex item with a fixed basis */
}
CSS

Two of these should sit side by side. In a 1000px container under content-box, each column is 500px of content plus 48px of padding, 548px in total. Two of them need 1096px, so the second one wraps underneath. There’s no fixed pixel value you can subtract to fix it, because the column width is a percentage and the padding is in pixels. Before box-sizing existed, people used calc(50% - 48px) or nested a padded inner div.

Under border-box, each column is exactly 500px including its padding, and two of them fit. The padding can be any unit without breaking the arithmetic.

The same thing makes width: 100% safe on form inputs. A text input with width: 100%; padding: 8px; border: 1px solid overflows its parent by 18px under content-box, which is why inputs poking out of their containers on the right is such a common sight in older stylesheets.

The global reset

Because border-box is easier almost everywhere, projects usually switch it on for everything. The version you’ll see most often is:

html {
  box-sizing: border-box;
}

*,
*::before,
*::after {
  box-sizing: inherit;
}
CSS

Why inherit instead of setting border-box on * directly? Because it lets a component switch back for its whole subtree with one declaration. If a third-party widget was built assuming content-box, you set .legacy-widget { box-sizing: content-box; } and all its descendants inherit that. With * { box-sizing: border-box; }, every descendant would have its own explicit border-box and you’d need to override each one.

The ::before and ::after selectors are there because * doesn’t match pseudo-elements. Without them, a decorative ::after with padding would still use content-box and could be off by its padding.

Some modern resets simply use *, *::before, *::after { box-sizing: border-box; }, and that’s also fine when you control all the CSS on the page. In an interview, explaining the trade-off is worth more than reciting either version.

Things that surprise people

Some elements are already border-box. Browsers’ user-agent stylesheets give several form controls border-box sizing (<select>, buttons and some input types, depending on the browser). That’s part of why a <select> and an <input> with the same width and padding could render at different sizes before the reset was common.

Content can’t go below zero. With border-box, if padding plus border exceed the width (width: 30px; padding: 20px), the content area would be −10px. Browsers clamp it to 0 and the box renders 40px wide, ignoring the width you asked for.

Inline elements ignore it. A <span> or <a> with the default display: inline doesn’t accept width or height at all, so box-sizing has nothing to act on. Its horizontal padding and border still push neighbouring text aside, and its vertical padding paints over the lines above and below without moving them. If you want a link to have a fixed size, give it display: inline-block (or make it a flex or grid item) first; then box-sizing applies as usual.

Min and max sizes follow the same model. min-width, max-width, min-height and max-height are measured in the same box that box-sizing selects. So max-width: 600px on a border-box element caps its border box at 600px.

Flex and grid sizing use it too. flex-basis: 200px refers to the box set by box-sizing, so the reset affects flex items’ starting sizes as well.

Outline and box-shadow aren’t part of any box. They’re painted outside the border and take no layout space, so they never change an element’s measured size, under either model. That’s why focus outlines don’t shift the layout when they appear.

Interview practice

A 320px wide container has two inputs side by side, each with width: 50%, padding: 10px and border: 2px solid. They wrap onto two lines. The page uses the default box-sizing. Explain what’s happening and give two fixes.

Answer

Under the default content-box, width: 50% sets each input’s content area to 160px. Padding adds 20px and borders add 4px, so each input is 184px wide and the pair needs 368px, which doesn’t fit in 320px, so the second one wraps.

The first fix is box-sizing: border-box on the inputs (or globally via the inherit reset), after which each input’s border box is exactly 160px with a 136px content area, and two fit exactly. The second fix, if you can’t change the box model, is width: calc(50% - 24px), which subtracts the padding and border so the total is back to 160px. The first is better because it doesn’t hard-code the padding and border values into the width, so changing the padding later doesn’t break the layout again. (A third option, putting the inputs in a flex or grid container with 1fr columns or flex: 1, also works, but the flex item’s basis still depends on box-sizing, so it’s good to understand the box model rather than reach around it.)

Keep going

More in HTML & CSS

read ✓HTML & CSS · mid

CSS Container Queries for Reusable Components

Build a card that switches layout based on the space it's given, not the viewport: container-type, named containers, cqi units and the gotchas.

~6 min readread →
read ✓HTML & CSS · mid

CSS Flex Items and Unexpected Overflow

Why a flex item holding a long URL or code block won't shrink, what min-width: auto computes, and when min-width: 0 or minmax(0, 1fr) fixes it.

~6 min readread →
read ✓HTML & CSS · mid

CSS Positioning and Containing Blocks

What top: 0 is measured from for each position value, how ancestors become containing blocks, and why transform breaks position: fixed.

~6 min readread →
esc