CSS Module 4 – The box model, sizing and overflow
The box model: content, padding, border, margin
How CSS sizes every element: the content, padding, border and margin areas, content-box or border-box, percentage padding and focus rings.
What you will learn
- Identify the content, padding, border and margin areas of a box in DevTools
- Choose between content-box and border-box sizing and apply border-box to a whole project
- Predict what percentage padding and padding on inline elements do to the layout
- Draw focus rings with outline and box-shadow without changing layout
What you’ll learn
What you will learn
- Identify the content, padding, border and margin areas of a box in DevTools
- Choose between content-box and border-box sizing and apply border-box to a whole project
- Predict what percentage padding and padding on inline elements do to the layout
- Draw focus rings with outline and box-shadow without changing layout
On this page
Every element on a page is laid out as a rectangle with four layers. In the middle is the content: text, an
image or child boxes. Around it comes the padding, then the border, and outside the border the margin,
which keeps neighbours at a distance. The width and height you write size only the content unless you change
box-sizing, so a card with width: 300px, 24 px of padding and a 1 px border is drawn 350 px wide. Knowing which
layer each property sets is what lets you hit the sizes in a design, space things predictably and draw focus rings
that move nothing.
This lesson takes one card through all four layers, with the numbers that Chromium 153, Firefox 155 and WebKit 26.6 reported for it.
The four areas of a box
The four areas of one box, with the widths of the lesson's card
Text description of the diagram
The diagram shows the card from the lesson, styled with width: 300px, padding: 24px, border: 1px and margin: 16px, and the default box-sizing: content-box. Four boxes sit one inside the other.
- The outermost box, drawn with a dashed line, is the margin box: 382 px wide, because it adds 16 px of margin on each side. Margins are always transparent.
- Inside it is the border box: 350 px wide, with 1 px of border on each side. This is the width offsetWidth reports.
- Inside the border is the padding box: 348 px wide, with 24 px of padding on each side. The element's background shows through here. This is the width clientWidth reports when there is no scrollbar.
- The innermost box is the content box: 300 px wide. Under content-box, width: 300px sets this box, so the padding and the border are added outside it.
- Content is where the text or the image goes. Under the default sizing,
widthandheightset its size. - Padding is the space between the content and the border. It belongs to the element, so the background colour or image shows through it, and it can’t be negative.
- Border is the line drawn around the padding. Its width takes up space just as padding does.
- Margin is the space outside the border. It is always transparent, it can be negative to pull boxes closer, and the vertical margins of neighbouring blocks can merge into one, which the next lesson covers.
Each layer ends at an edge, and each edge encloses a box: the content box, the padding box, the border box and the
margin box. Browsers report the border box as the element’s size in most places, so that is the number to check
first. The CSSOM gives you two quick measurements: offsetWidth is the width of the border box and clientWidth the
width of the padding box without any scrollbar, both as whole numbers of pixels, while getBoundingClientRect()
keeps the fractions.
To see the four areas of any element, select it in DevTools. Chrome and Edge draw the box-model diagram in the Computed tab and in the sidebar of the Styles tab, Firefox in the Box Model view of its Layout panel, and Safari in its Computed panel, and hovering over the element’s markup shades its content, padding and margin on the page. The values in the diagram can be edited in place, which is the quickest way to try a spacing change.
content-box or border-box
Three cards share width, 24 px of padding and a 1 px border, and differ only in box-sizing. The script at the
end prints what the browser drew:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>content-box and border-box</title>
<style>
body { font: 16px/1.5 system-ui, sans-serif; margin: 16px; }
.card {
width: 300px;
padding: 24px;
border: 1px solid #555;
margin-block-end: 16px;
}
.border-box { box-sizing: border-box; }
.too-narrow { box-sizing: border-box; width: 40px; }
</style>
</head>
<body>
<div class="card" id="a">content-box, the default</div>
<div class="card border-box" id="b">border-box</div>
<div class="card too-narrow" id="c">40</div>
<script>
// offsetWidth is the border box; clientWidth is the padding box (minus any scrollbar).
for (const id of ['a', 'b', 'c']) {
const el = document.getElementById(id);
console.log(`${id}: width ${getComputedStyle(el).width}, border box ${el.offsetWidth}px, padding box ${el.clientWidth}px`);
}
</script>
</body>
</html> Cards a and b are width: 300px and card c is width: 40px. All three engines printed the same numbers, the border
box from offsetWidth and the padding box from clientWidth:
| Card | Border box | Padding box |
|---|---|---|
| a, content-box | 350 px | 348 px |
| b, border-box | 300 px | 298 px |
| c, border-box, 40px | 50 px | 48 px |
Under content-box, the default, width is the content width, and the padding and border of both sides are
added outside it: 300 + 2 × 24 + 2 × 1 = 350. Under border-box, width is the border box, and the content gets
what is left: 300 − 50 = 250 px. That is the size a designer means, which is why most projects switch to it.
Card c shows the one exception. Padding and border can’t be squeezed, and the content box can’t go below zero, so a
40 px border-box card with 50 px of padding and border is drawn 50 px wide. getComputedStyle() reports the used
width in the same terms, so it said 300px for both a and b, and 50px for c.
The same sums, as a program you can change and run:
// The box model as arithmetic: the width of each of the four boxes of an element, from the lengths in its CSS
// (all in px). Change the numbers and run it again: the browser does the same sums for every element.
/** The four boxes of one element along one axis. */
function boxes({ width, padding, border, margin, boxSizing }) {
const sides = 2; // left and right (top and bottom work the same way)
const around = (padding + border) * sides;
// border-box: the width you write includes padding and border, and the content box is never below 0.
const content = boxSizing === 'border-box' ? Math.max(0, width - around) : width;
const paddingBox = content + padding * sides;
const borderBox = paddingBox + border * sides;
const marginBox = borderBox + margin * sides;
return { content, paddingBox, borderBox, marginBox };
}
const card = { width: 300, padding: 24, border: 1, margin: 16 };
console.log('width: 300px; padding: 24px; border: 1px; margin: 16px');
console.log('box-sizing content padding border margin');
for (const boxSizing of ['content-box', 'border-box']) {
const b = boxes({ ...card, boxSizing });
const cells = [b.content, b.paddingBox, b.borderBox, b.marginBox].map((n, i) => String(n).padStart(i < 2 ? 7 : 6));
console.log(`${boxSizing.padEnd(13)} ${cells.join(' ')}`);
}
// When padding and border need more room than the width allows, they win and the box grows.
const narrow = boxes({ ...card, width: 40, boxSizing: 'border-box' });
console.log(`border-box, width: 40px -> content ${narrow.content}, border box ${narrow.borderBox}`);
// Percentages of padding and margin resolve against the WIDTH of the containing block, even top and bottom.
const percentOf = (percent, containingWidth) => (percent / 100) * containingWidth;
for (const containingWidth of [640, 360]) {
console.log(`containing block ${containingWidth}px wide: padding-top: 56.25% = ${percentOf(56.25, containingWidth)}px`);
} Output
width: 300px; padding: 24px; border: 1px; margin: 16px box-sizing content padding border margin content-box 300 348 350 382 border-box 250 298 300 332 border-box, width: 40px -> content 0, border box 50 containing block 640px wide: padding-top: 56.25% = 360px containing block 360px wide: padding-top: 56.25% = 202.5px
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node box_model.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
box-sizing affects every sizing property: width, height, their min- and max- limits, and flex-basis. It
never changes margins, which always sit outside the border box.
Border-box for a whole project
/* One rule for the whole project: width and height include padding and border. */
html {
box-sizing: border-box;
}
/* Every element and pseudo-element takes its parent's value, so a component that needs content-box can switch its
own root back, and everything inside it follows. */
*,
*::before,
*::after {
box-sizing: inherit;
} box-sizing is not inherited, so the second rule asks every element and pseudo-element to take its parent’s value.
The result is border-box everywhere, with one escape hatch: a component that was built for content-box, such as an
older widget, gets box-sizing: content-box on its own root, and everything inside it follows. A plain
* { box-sizing: border-box; } works too, but then each element inside such a widget needs its own rule.
Percentages use the containing block’s width
Percentage padding and percentage margins resolve against the width of the containing block (its inline size, in horizontal writing), even on the top and bottom sides. The frame below is 640 px wide and only 100 px tall:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Percentage padding and aspect-ratio</title>
<style>
body { margin: 0; }
.frame { inline-size: 640px; block-size: 100px; }
.half { padding-block-start: 50%; background: #dde; }
.ratio-padding { block-size: 0; padding-block-start: 56.25%; background: #ded; }
.ratio-modern { aspect-ratio: 16 / 9; background: #edd; }
</style>
</head>
<body>
<div class="frame">
<div class="half" id="half"></div>
<div class="ratio-padding" id="old"></div>
<div class="ratio-modern" id="new"></div>
</div>
<script>
const report = () => {
const w = document.querySelector('.frame').offsetWidth;
const h = (id) => document.getElementById(id).getBoundingClientRect().height;
console.log(`frame ${w}px wide, 100px tall: 50% padding ${h('half')}px, 56.25% padding ${h('old')}px, aspect-ratio ${h('new')}px`);
};
report();
document.querySelector('.frame').style.inlineSize = '360px';
report();
</script>
</body>
</html> | Frame | 50 % padding | 56.25 % padding | 16 / 9 ratio |
|---|---|---|---|
| 640 px wide | 320 px | 360 px | 360 px |
| 360 px wide | 180 px | 202.5 px | 202.5 px |
The frame was 100 px tall in both runs, yet the 50 % padding is half of 640, not half of 100. Before aspect-ratio existed, this rule was the standard way to
build a box with a fixed ratio: an empty element with padding-top: 56.25% is always 16:9, because 56.25 % is
9 ÷ 16. The trick needs a height of 0 and absolutely positioned children, and its padding is unusable for anything
else. aspect-ratio, which every current engine supports, says the same thing directly and leaves padding free, so
use it in new code; the sizing lesson in this module shows it with images.
Padding and borders on inline elements
A span, an a or an em in a paragraph generates an inline box, and inline boxes follow different rules
in the vertical direction. Their padding and borders are painted, but the height of each line is still worked out
from line-height alone, and vertical margins have no effect at all. Three paragraphs with 24 px lines and a word
on line 2 show it; in the last two the word gets 12 px of padding above and below, a border and 30 px margins:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Padding on inline boxes</title>
<style>
body { margin: 0; }
p { margin: 0 0 20px; font: 16px/24px Arial, sans-serif; }
.tag { padding-block: 12px; border: 1px solid #555; background: #ffd; margin-block: 30px; }
.tag.atomic { display: inline-block; }
</style>
</head>
<body>
<p id="plain">The first line of the paragraph,<br><span>a word</span> on the second line<br>and a third line.</p>
<p id="padded">The first line of the paragraph,<br><span class="tag">a word</span> on the second line<br>and a third line.</p>
<p id="inline-block">The first line of the paragraph,<br><span class="tag atomic">a word</span> on the second line<br>and a third line.</p>
<script>
// Each line box is 24px tall, so line 2 starts 24px below the paragraph's top and ends at 48px.
const px = (n) => Math.round(n * 100) / 100; // engines measure in fractions of a pixel
for (const id of ['plain', 'padded', 'inline-block']) {
const p = document.getElementById(id);
const box = p.querySelector('span').getBoundingClientRect();
const top = px(box.top - p.getBoundingClientRect().top);
console.log(`${id}: paragraph ${p.offsetHeight}px tall; the span paints from ${top}px to ${px(top + box.height)}px`);
}
</script>
</body>
</html> | The word | Paragraph height | Word box paints |
|---|---|---|
| plain | 72 px | 27 to 44 px |
| padded | 72 px | 14 to 57 px |
| padded, inline-block | 158 px | 54 to 104 px |
Line 2 runs from 24 to 48 px. The padded word paints about 10 px into the line above and the line below while every
line stays where it was, so its
background covers part of the neighbouring text. The 30 px margins changed nothing. The rows are Chromium’s numbers;
Firefox drew the word’s box 2 px taller, because each engine reads the height of the text from the font in its own
way, and all three kept the paragraph at 72 px. As an inline-block, the word becomes one solid box in the line: its padding, border and
margins all take space, so line 2 grows to 30 + 50 + 30 = 110 px and the paragraph to 158 px.
Use plain inline padding for a light highlight that may overlap, and keep it small. When the box needs real room,
such as a badge in a sentence or a link that should be easy to tap, make it inline-block or inline-flex.
Horizontal padding, borders and margins of inline boxes do take room, so they push the next word along the line.
Focus rings that move nothing
A keyboard user needs to see which control has focus, and the ring that shows it must not shift the page. Three common ways to draw it were added in turn to a button with a 1 px border, and its neighbour was measured: a border thickened to 3 px, a 3 px outline with a 2 px offset, and a 3 px spread shadow.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Focus styles that move nothing</title>
<style>
body { margin: 16px; font: 16px/1.5 Arial, sans-serif; }
button { font: inherit; padding: 8px 16px; border: 1px solid #555; border-radius: 8px; margin-inline-end: 8px; }
/* Three ways to draw a focus ring; the script adds each class in turn. */
.ring-border { border-width: 3px; }
.ring-outline { outline: 3px solid #1a5fb4; outline-offset: 2px; }
.ring-shadow { box-shadow: 0 0 0 3px #1a5fb4; }
</style>
</head>
<body>
<button id="first">Save</button><button id="next">Cancel</button>
<script>
const first = document.getElementById('first');
const next = document.getElementById('next');
const before = { width: first.offsetWidth, left: next.offsetLeft };
for (const ring of ['ring-border', 'ring-outline', 'ring-shadow']) {
first.classList.add(ring);
console.log(`${ring}: Save grows by ${first.offsetWidth - before.width}px, Cancel moves by ${next.offsetLeft - before.left}px`);
first.classList.remove(ring);
}
</script>
</body>
</html> | Ring | Grows by | Next button moves |
|---|---|---|
| 3 px border | 4 px | 4 px |
| 3 px outline | 0 px | 0 px |
| 3 px shadow | 0 px | 0 px |
A thicker border changes the border box, so everything after it moves on every focus change. An outline is drawn
outside the border and takes no space; outline-offset sets the gap between them, and current engines draw it
around the border-radius, which all three did in the run. A box-shadow with a spread and no blur takes no space
either, and also follows the radius.
The difference shows in forced colours mode, which the contrast themes of Windows turn on, where the browser
replaces the page’s colours with the user’s. That mode removes every box-shadow, so a ring drawn only with a shadow disappears,
while an outline’s colour is replaced with a system colour and stays visible, even when the outline was
transparent:
/* A ring for keyboard focus that takes no space: the outline is drawn outside the border and moves nothing. */
:focus-visible {
outline: 3px solid #1a5fb4;
outline-offset: 2px;
}
/* A ring drawn with box-shadow moves nothing either, but forced colours mode (the contrast themes of Windows) removes
every box-shadow. The transparent outline then gets a system colour, so a ring is still drawn. */
.chip:focus-visible {
outline: 2px solid transparent;
box-shadow: 0 0 0 3px #1a5fb4;
} Use :focus-visible rather than :focus: it matches when the browser decides that focus should be shown, which
includes keyboard focus but usually not a mouse click on a button. Never remove the outline without drawing something
in its place. WCAG 2.2 requires a visible focus indicator at level AA (success criterion 2.4.7), and its level AAA
criterion 2.4.13 asks for an indicator with at least the area of a 2 px thick perimeter around the control, whose
pixels change with a contrast of at least 3:1 between the unfocused and the focused state.
Logical names: margin-inline and padding-block
Many current stylesheets, and the examples on this page, write margin-inline, padding-block or inline-size instead of
margin-left, padding-top or width. These logical properties name the sides by the direction of the text:
in English, inline means left and right and block means top and bottom, so padding-block: 12px sets the top and
bottom padding. In a right-to-left language or vertical text they follow the text, which is why they are worth the
habit. The international layout module of this track covers them in full.
Practise: hit an exact size
Designs give sizes as they appear on screen. Before writing CSS for them, it helps to work out what to type.
Exercise · Easy · JavaScript
Work out the width to write for an exact card size
A design gives the size of every card as it appears on screen: its border box. Before you write the CSS, work out the numbers. Write two functions in sizes.mjs and export them. Each takes one object; padding and border are the widths on each side, in px, and boxSizing is 'content-box' or 'border-box' (when it is missing, the default 'content-box' applies). The same sums work for widths and for heights.
The first function, renderedSize({ size, padding, border, boxSizing }), returns the size of the border box a browser draws for width: <size>px (or height). Under border-box the content box can't be smaller than 0, so when the padding and border need more room than size, they win: renderedSize({ size: 40, padding: 24, border: 1, boxSizing: 'border-box' }) is 50.
The second, sizeToWrite({ target, padding, border, boxSizing }), returns the number to write in width (or height) so that the border box is exactly target px. For a 320 px card with 24 px of padding and a 1 px border that is 270 under content-box and 320 under border-box. When no value can give that size, because the padding and borders of the two sides alone are wider than target, throw a RangeError.
The sample tests import both functions from sizes.mjs and run in your browser.
Starter code · sizes.mjs
/** The border-box size (px) a browser draws for `width: <size>px` with this padding and border on each side. */
export function renderedSize({ size, padding, border, boxSizing = 'content-box' }) {
// Replace this line with your code.
return size;
}
/** The value (px) to write in width or height so that the border box is exactly `target` px. */
export function sizeToWrite({ target, padding, border, boxSizing = 'content-box' }) {
// Replace this line with your code.
return target;
} The sample tests · sizes.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { renderedSize, sizeToWrite } from './sizes.mjs';
const card = { padding: 24, border: 1 };
test('content-box adds the padding and the border of both sides', () => {
assert.equal(renderedSize({ size: 300, ...card, boxSizing: 'content-box' }), 350);
assert.equal(renderedSize({ size: 200, padding: 10, border: 2, boxSizing: 'content-box' }), 224);
});
test('content-box is the default when boxSizing is missing', () => {
assert.equal(renderedSize({ size: 300, ...card }), 350);
assert.equal(sizeToWrite({ target: 320, ...card }), 270);
});
test('border-box draws the size you write', () => {
assert.equal(renderedSize({ size: 300, ...card, boxSizing: 'border-box' }), 300);
assert.equal(renderedSize({ size: 51, ...card, boxSizing: 'border-box' }), 51);
});
test('under border-box, padding and border win when they need more room', () => {
assert.equal(renderedSize({ size: 40, ...card, boxSizing: 'border-box' }), 50);
assert.equal(renderedSize({ size: 0, padding: 8, border: 0.5, boxSizing: 'border-box' }), 17);
});
test('the width to write for a 320 by 200 px card', () => {
assert.equal(sizeToWrite({ target: 320, ...card, boxSizing: 'content-box' }), 270);
assert.equal(sizeToWrite({ target: 200, ...card, boxSizing: 'content-box' }), 150);
assert.equal(sizeToWrite({ target: 320, ...card, boxSizing: 'border-box' }), 320);
});
test('a target exactly as wide as padding plus border leaves no room for content', () => {
assert.equal(sizeToWrite({ target: 50, ...card, boxSizing: 'content-box' }), 0);
assert.equal(sizeToWrite({ target: 50, ...card, boxSizing: 'border-box' }), 50);
});
test('a target smaller than padding plus border throws a RangeError', () => {
assert.throws(() => sizeToWrite({ target: 49, ...card, boxSizing: 'content-box' }), RangeError);
assert.throws(() => sizeToWrite({ target: 49, ...card, boxSizing: 'border-box' }), RangeError);
});
test('what you write is what renders, in both models', () => {
for (const boxSizing of ['content-box', 'border-box']) {
for (const target of [50, 64, 199.5, 320, 1000]) {
const size = sizeToWrite({ target, ...card, boxSizing });
assert.equal(renderedSize({ size, ...card, boxSizing }), target, `${boxSizing}, ${target}px`);
}
}
}); A hint
Both functions need the room that padding and border take on the two sides together: 2 * (padding + border). Under content-box, renderedSize adds that room to size and sizeToWrite subtracts it from target. Under border-box, the size you write is the border box, unless the room alone is larger: then the box is that room.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Interview questions
Fresher · What is the difference between box-sizing: content-box and border-box, and why do many projects set border-box everywhere?
With content-box, the default, width and height set the content area, and padding and border are added
outside it, so a 300 px wide box with 24 px of padding and a 1 px border is drawn 350 px wide. With border-box,
width and height set the border box: the padding and border are drawn inside, and the content gets what is left,
250 px in the same example, never less than zero. Projects switch to border-box because designs give the size you
see, padding can change without breaking a layout, and width: 100% plus padding no longer overflows its container.
The usual setup puts box-sizing: border-box on html and inherit on every element, so a component that needs
content-box can switch back in one place.
Key takeaways
- Every box has four areas: content, padding, border and margin. Margins are transparent and may be negative; padding may not.
content-box(the default) adds padding and border outsidewidth;border-boxdraws them inside it, and the content box never goes below zero.- Put
box-sizing: border-boxonhtmlandbox-sizing: inheriton every element and pseudo-element. - Percentage padding and margins resolve against the containing block’s width, even on the top and bottom; use
aspect-ratiofor boxes with a fixed ratio. - Vertical padding and borders on inline boxes paint over the neighbouring lines without moving them; use
inline-blockwhen they need room. - Draw focus rings with
outlineandoutline-offseton:focus-visible; if you use abox-shadow, add a transparent outline so forced colours mode still shows a ring.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- CSS Box Model Module Level 4 (W3C CSS Working Group)
- CSS Box Sizing Module Level 3: the box-sizing property (W3C CSS Working Group)
- CSS 2: visual formatting model details (inline, non-replaced elements) (W3C CSS Working Group)
- CSS Basic User Interface Module Level 4: outline properties (W3C CSS Working Group)
- CSS Color Adjustment Module Level 1: properties affected by forced colors mode (W3C CSS Working Group)
- CSS Backgrounds and Borders Module Level 3: drop shadows (W3C CSS Working Group)
- CSS Box Sizing Module Level 4: aspect-ratio (W3C CSS Working Group)
- Selectors Level 4: the :focus-visible pseudo-class (W3C CSS Working Group)
- CSSOM View Module: offsetWidth and clientWidth (W3C CSS Working Group)
- Web Content Accessibility Guidelines (WCAG) 2.2: Focus Visible and Focus Appearance (W3C)
- Introduction to the CSS box model (MDN Web Docs) (Mozilla)
- Examine and edit the box model (Firefox DevTools) (Mozilla)
- Elements tab (Web Inspector Reference) (WebKit)
- CSS features reference (Chrome DevTools) (Google)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress