The Box Model & Layout
Every element the browser renders becomes a rectangular box. Layout is the process of sizing those boxes and placing them. Understanding the box and the default flow removes most of the guesswork from CSS.
The box model
Section titled “The box model”Four concentric layers, from the inside out:
┌─────────────────── margin (transparent, outside) ────────────┐│ ┌───────────────── border ───────────────────────────────┐ ││ │ ┌───────────── padding (inside, painted by bg) ─────┐ │ ││ │ │ │ │ ││ │ │ content │ │ ││ │ │ │ │ ││ │ └───────────────────────────────────────────────────┘ │ ││ └────────────────────────────────────────────────────────┘ │└──────────────────────────────────────────────────────────────┘- Content box — where text and child boxes go.
width/heighttarget this box by default. - Padding box — space inside the border. The element’s
backgroundpaints here (and under the border). - Border box — the visible edge.
- Margin box — space outside the border. Always transparent; the parent’s background shows through.
.box { width: 300px; padding: 20px; border: 2px solid black; margin: 16px;}/* Rendered width of the border box: 300 + 20 + 20 + 2 + 2 = 344px *//* Space it occupies in flow: 344 + 16 + 16 = 376px */Shorthand order
Section titled “Shorthand order”padding: 10px; /* all four sides */padding: 10px 20px; /* block (top/bottom) | inline (left/right) */padding: 10px 20px 5px; /* top | left+right | bottom */padding: 10px 20px 5px 0; /* top | right | bottom | left (clockwise) */margin and border-width use the same pattern. Mnemonic for the four-value form: TRouBLe — Top, Right, Bottom, Left.
box-sizing: border-box
Section titled “box-sizing: border-box”box-sizing changes what width and height mean.
| Value | width: 300px means |
|---|---|
content-box (default) |
content is 300px; padding and border add on top |
border-box |
the border box is 300px; padding and border eat into the content |
Content-box makes width: 100% plus any padding overflow its parent. Set border-box once, globally:
*, *::before, *::after { box-sizing: border-box; }The *::before, *::after part matters — pseudo-elements do not inherit box-sizing from * because * does not match them.
Sizing constraints
Section titled “Sizing constraints”.panel { width: 100%; max-width: 60ch; /* caps it */ min-height: 10rem; height: auto;}min-width/min-height beat max-*, which beat width/height. Also useful:
.thing { width: min-content; /* narrowest without overflowing (longest word) */ width: max-content; /* widest it wants to be, no wrapping */ width: fit-content; /* max-content, capped by the available space */}fit-content is the clean way to shrink-wrap a block element to its content while keeping it a block.
Normal flow
Section titled “Normal flow”With no positioning, floats, flex, or grid, boxes lay out in normal flow:
- Block boxes stack vertically, each taking the full available inline width, in source order.
- Inline boxes flow horizontally inside a block, wrapping onto new line boxes as needed.
That is the entire default. Flex and Grid replace normal flow inside the element they’re set on; the element itself is still a box in its parent’s flow.
display
Section titled “display”display decides two things: how the box behaves in its parent (outer display type) and how its children are laid out (inner display type).
| Value | Outer | Inner | Notes |
|---|---|---|---|
block |
block | flow | full width, respects all box properties |
inline |
inline | flow | flows with text |
inline-block |
inline | flow-root | flows with text, but is a block internally |
flex |
block | flex | children become flex items |
inline-flex |
inline | flex | |
grid |
block | grid | children become grid items |
inline-grid |
inline | grid | |
flow-root |
block | flow-root | block that contains its floats and blocks margin collapse |
none |
— | — | box is not generated at all |
contents |
— | — | element’s own box is removed; children stay |
Modern CSS also accepts the explicit two-value form: display: inline flex is the same as display: inline-flex.
Inline elements ignore some properties
Section titled “Inline elements ignore some properties”span { width: 200px; /* ignored */ height: 50px; /* ignored */ margin-block: 20px;/* ignored for layout */ padding-block: 20px;/* painted, but does NOT push other lines away */}Vertical padding and borders on an inline box are drawn, but they overlap surrounding lines instead of affecting line height. If you need box behaviour in a text flow, use inline-block.
none vs visibility: hidden vs opacity: 0
Section titled “none vs visibility: hidden vs opacity: 0”| Takes up space | In the accessibility tree | Clickable | |
|---|---|---|---|
display: none |
no | no | no |
visibility: hidden |
yes | no | no |
opacity: 0 |
yes | yes | yes |
An opacity: 0 element is still focusable and clickable — a common source of invisible click targets.
display: contents
Section titled “display: contents”Removes the element’s own box while keeping its children in the layout. Useful for making a wrapper’s children into direct flex/grid items:
<div class="grid"> <div style="display: contents"> <div>a</div> <div>b</div> </div></div>Both inner divs become grid items of .grid.
Margin collapsing
Section titled “Margin collapsing”Adjacent vertical margins in normal flow collapse into one, whose size is the larger of the two (or, with a negative margin, the sum of the largest positive and the most negative).
This happens in exactly three situations:
1. Adjacent siblings
<p style="margin-bottom: 30px">A</p><p style="margin-top: 20px">B</p><!-- Gap is 30px, not 50px -->2. Parent and first/last child — if nothing separates them:
<div class="parent"> <!-- no padding, no border --> <p style="margin-top: 20px">Child</p></div><!-- The 20px escapes the parent and pushes the PARENT down -->3. Empty blocks — a block with no content, padding, border or height collapses its own top and bottom margins together.
Preventing it
Section titled “Preventing it”Collapsing stops when something sits between the margins:
.parent { padding-block-start: 1px; } /* anything non-zero */.parent { border-block-start: 1px solid transparent; }.parent { display: flow-root; } /* cleanest: new formatting context */.parent { overflow: hidden; } /* works, but clips */Margins also never collapse in these contexts:
- Flex and grid containers — flex/grid items never collapse margins with anything.
- Floated or absolutely positioned elements.
- Elements that establish a new block formatting context (BFC).
Block formatting contexts
Section titled “Block formatting contexts”A BFC is a region of the page that lays out independently: floats inside it do not escape, floats outside do not intrude, and its margins do not collapse with the outside.
A BFC is created by, among others: the root element, floats, absolutely positioned elements, display: inline-block, display: flow-root, table cells, flex/grid items, and any block container with overflow other than visible.
display: flow-root exists specifically so you can create one without side effects. The old overflow: hidden clearfix works only because it makes a BFC.
.clearfix { display: flow-root; } /* contains floated children */Positioning
Section titled “Positioning”position changes how a box is placed and what top/right/bottom/left (the inset properties) mean.
static (default)
Section titled “static (default)”Normal flow. Inset properties and z-index are ignored.
relative
Section titled “relative”Laid out in normal flow, then visually shifted by the insets. The space it originally occupied is preserved — surrounding content does not move.
.nudge { position: relative; top: -2px; }Its main job in practice is to become a positioning ancestor for absolutely positioned children.
absolute
Section titled “absolute”Removed from normal flow entirely (siblings behave as if it doesn’t exist), then positioned against its containing block: the padding box of the nearest ancestor whose position is not static. If there is none, it positions against the initial containing block (viewport-sized, at the document origin).
.card { position: relative; }.card .badge { position: absolute; inset-block-start: 8px; inset-inline-end: 8px;}With no insets specified, an absolute box sits at its static position — where it would have been in flow — which is often exactly what you want.
Setting opposite insets stretches it:
.overlay { position: absolute; inset: 0; } /* = top/right/bottom/left: 0 */Removed from flow and positioned against the viewport, so it does not scroll.
.header { position: fixed; inset-block-start: 0; inset-inline: 0; }sticky
Section titled “sticky”A hybrid: laid out in normal flow, but once its scroll container reaches the given offset it stops moving and behaves like fixed — until its parent scrolls out of view.
th { position: sticky; inset-block-start: 0; background: white; /* it will scroll over content, so it needs one */}Requirements people miss:
- You must specify at least one inset (
top,bottom,left, orright). A sticky element with no offsets never sticks. - It sticks within its parent. It can never leave the parent’s box, so a sticky element in a short parent barely moves.
- No ancestor between it and the scroll container may have
overflow: hidden,scroll, orauto. That ancestor becomes the scroll container, and the element sticks to that instead — usually invisibly.
Containing block summary
Section titled “Containing block summary”position |
Containing block |
|---|---|
static, relative, sticky |
content box of the nearest block-container ancestor |
absolute |
padding box of the nearest ancestor with position != static |
fixed |
the viewport, unless a transformed/filtered/contained ancestor intercepts it |
This is also why percentage widths resolve differently for absolute elements: width: 50% on an absolute child is 50% of the ancestor’s padding box, not its content box.
z-index and stacking contexts
Section titled “z-index and stacking contexts”Boxes paint in a defined order. Within a stacking context, painting proceeds roughly like this, back to front:
- The context root’s own background and borders
- Descendants with negative
z-index - In-flow, non-positioned block boxes
- Floats
- In-flow inline content
- Positioned descendants with
z-index: autoor0 - Descendants with positive
z-index
z-index only applies to positioned elements — and to flex and grid items, which accept z-index even when position: static.
The trap
Section titled “The trap”A stacking context is self-contained. Its children’s z-index values are only compared with each other, never with elements outside. So a child with z-index: 999999 can still paint below an unrelated element if its ancestor’s stacking context sits lower.
<div class="a"> <!-- z-index: 1 --> <div class="modal"> <!-- z-index: 99999 --></div><div class="b"></div> <!-- z-index: 2 -> paints above the whole of .a -->.modal loses. 99999 is compared only against .a’s other children.
What creates a stacking context
Section titled “What creates a stacking context”- The root element (
<html>) position: relative|absolutewithz-indexother thanautoposition: fixedorsticky(always, regardless ofz-index)- A flex or grid item with
z-indexother thanauto opacityless than 1transform,filter,backdrop-filter,perspective,clip-path,mask,mix-blend-modeother thannormalisolation: isolatecontain: layout|paint|strict|contentwill-changenaming any property that would create one
Practical rule: keep a small, documented set of z-index values (say 10, 20, 30, 40) as custom properties, and isolate components.
:root { --z-dropdown: 10; --z-sticky: 20; --z-overlay: 30; --z-modal: 40; --z-toast: 50;}overflow
Section titled “overflow”Controls what happens when content is larger than its box.
| Value | Behaviour |
|---|---|
visible (default) |
content spills out, still visible and interactive |
hidden |
clipped; programmatically scrollable (JS can set scrollTop) |
clip |
clipped; no scrolling at all, not even programmatic |
scroll |
clipped, scrollbars always present |
auto |
clipped, scrollbars appear only when needed |
.pane { overflow: auto; } /* both axes */.row { overflow-x: auto; overflow-y: hidden; }Details that bite:
overflow: hidden|scroll|autoestablishes a BFC and makes the element a scroll container, which breaksposition: stickyfor descendants relative to the page.- If one axis is
visibleand the other is not, thevisibleone computes toauto. You cannot clip horizontally while overflowing vertically — except withoverflow-x: clip; overflow-y: visible, which does work. overflow-clip-marginletsclipkeep a few pixels of bleed:overflow: clip; overflow-clip-margin: 4px;.
Related:
.scroller { overscroll-behavior: contain; /* stop scroll chaining to the page */ scroll-behavior: smooth; /* smooth anchor/scrollTo jumps */ scrollbar-gutter: stable; /* reserve space so layout doesn't shift */}Text overflow is separate:
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}All three are required — text-overflow alone does nothing.
Key points
Section titled “Key points”box-sizing: border-boxglobally, always.- Inline boxes ignore width, height, and vertical margins; vertical padding paints but does not push.
- Vertical margins collapse between siblings and between a parent and its first/last child;
display: flow-root, padding, a border, or a flex/grid container stops it. absolutepositions against the nearest non-static ancestor’s padding box;fixedpositions against the viewport unless a transformed ancestor intercepts it.stickyneeds an inset, sticks only within its parent, and dies inside a scrolling ancestor.z-indexis compared only within a stacking context;isolation: isolateis how you sandbox one.