Formatting contexts and CSS positioningThe box model and normal flow

100%

The box model and normal flow.

Before anything is positioned, layered or animated, the browser turns each element into a set of nested rectangles and drops them into the page by two simple rules: blocks stack, text wraps into lines. Most of the spacing surprises in a real app, a card a few pixels too wide, a gap that is 24 instead of 40, a heading that pushes its whole column down, come from those two rules and nothing else.

Beginner32 minUpdated 2 Oct 2026

The idea.

Layout starts by turning elements into rectangles. How big each one is, and where the next one goes, follows from a handful of rules you can do in your head.

Slatework's design file says a task card is 320 pixels wide with 16 pixels of padding and a hairline border. A developer types exactly that into the stylesheet, opens the board, and the card is 354 pixels wide and sticks out of its column. Nothing is broken. The browser did what the declaration asked, because by default width means the width of the content only, and padding and border are added outside it.

That is the whole topic in one bug. Before the browser paints anything it builds a tree of boxes from the elements and their computed styles. Each box has a content area for text and children, then padding, then a border, then a margin that keeps neighbours away. Its final size comes from three inputs: what it contains, what its own properties say, and the width its parent makes available.

Then the boxes are placed. Without flex, grid, floats or positioning, the browser uses normal flow: block-level boxes go one under the other, each as wide as its parent allows; inline content such as text and links runs along a line and wraps onto the next one when it runs out of room. Two quirks of that flow, percentages measured against the parent and vertical margins that merge instead of adding up, explain most of the spacing that does not match the mockup.

Where a box's size comes from

Its content
Text, images and child boxes. A block's height grows with what is inside it unless something fixes it.
Its own properties
width, height, padding, border and margin, and box-sizing, which decides whether width counts the padding and border.
Its containing block
Usually the parent's content box. A block box with width auto fills it, and percentages are measured against its width.

One Slatework card, as boxes

width: 320px sizes the content box only. Padding and border are added outside it, so the border box is 320 + 32 + 2 = 354 pixels and the margin box 378.

content-box (the default). Box layout “article.card”: 3 shapes. View: 412 × 196 from -16, -28. 1. “margin box” (rect) badge “margin: 12px” 378 × 146 at 0, 0 yellow 2. “border box” (rect) badge “1px border + 16px padding” 354 × 122 at 12, 12 green in margin box 3. “content box” (rect) badge “width: 320px” says “Fix flaky login test” 320 × 88 at 29, 29 blue in border box

  1. What the reader sees as the card2TaskCard

Where the pixels of one card go, across the inline axis

content-box: 378 in all
  1. width 12, Margin, value 12
  2. width 1, Border
  3. width 16, Padding, value 16
  4. width 320, Content, value 320 (the declared width)
  5. width 16, Padding, value 16
  6. width 1, Border
  7. width 12, Margin, value 12
  • border box 354, from 12 to 366
border-box: 344 in all
  1. width 12, Margin, value 12
  2. width 1, Border
  3. width 16, Padding, value 16
  4. width 286, Content, value 286
  5. width 16, Padding, value 16
  6. width 1, Border
  7. width 12, Margin, value 12
  • border box 320 = width, from 12 to 332
  • Margin
  • Border
  • Padding
  • Content
The same declarations (width 320, padding 16, border 1, margin 12) under both box-sizing values. Margin is outside the box under either value; only padding and border move.
Was this section helpful?

How it works.

Four steps, in the order the browser applies them. display decides which boxes exist, box-sizing and the containing block decide their size, normal flow places them, and margin collapsing adjusts the space between them.

1

display has two halves

It is tempting to read display: flex as a description of the element itself. It is really two answers packed into one word. The outer display type says how this box behaves among its siblings: a block-level box takes a line of its own across the parent; an inline-level box sits in a line of text next to its neighbours. The inner display type says how this box arranges its own children: flow (ordinary block and inline layout), flow-root (the same, but sealed off from the outside, which the next topic explains), flex, grid or table.

CSS Display Level 3 lets you write both halves, so display: inline flex is a box that sits in a line of text and lays out its children as a flex row. The familiar single keywords are short forms of the pairs: block means block flow, inline-block means inline flow-root, flex means block flex. Every current engine accepts the two-word form (Chrome since 115, Firefox since 70, Safari since 15), but for everyday layout it adds nothing the single keywords cannot say, so most codebases keep them.

Two values produce no box of their own. display: none removes the element and its whole subtree from layout. display: contents removes only the element's own box and promotes its children, as if the wrapper were not there; it is handy for a wrapper a framework forces on you, but check how the element is exposed to assistive technology, which has had browser bugs. The render pipeline topic covers how this box tree is built from the DOM and styles.

The keywords you use, split into their two halves

You writeOuter (among siblings)Inner (for its children)What you get in Slatework
blockblockflowA task card: takes the full width of its column, children stack inside
inlineinlineflowA link or an @mention inside a comment: flows with the text
inline-blockinlineflow-rootThe QA tag chip if you want it to keep its own height and padding in the line
flow-rootblockflow-rootA column that must contain its children's margins and floats
flexblockflexThe card's meta row: avatar, comment count and due date side by side
inline-flexinlineflexA button with an icon and a label, sitting in a sentence
gridblockgridThe board: columns side by side
contents(no box)children promotedA wrapper div the component library adds around list items
none(no box)(no box)A collapsed filter panel: nothing laid out, nothing announced

A card's elements and the boxes they generate

A card's elements and the boxes they generate
A card's elements and the boxes they generateParts: article.card, h3 Fix flaky login test, text: Due Fri, span.tag QA, span.draft-flag (display: none), ul.meta, Block box (article), Block box (h3), Anonymous block box, Inline boxes: text run + span, Block box (ul).

Box treewhat layout works on

DOMwhat the HTML says

generates

generates

generates

generates

generates

contains

wraps

article.card

h3 Fix flaky login test

text: Due Fri

span.tag QA

span.draft-flag (display: none)

ul.meta

Block box (article)

Block box (h3)

Anonymous block box
no element behind it

Inline boxes: text run + span

Block box (ul)

4 steps. Step through to see what one change does to these components.

2

Two directions, named by the text

Normal flow moves in two directions, and CSS names them after the writing rather than the screen. The inline direction is the way a line of text runs. The block direction is the way lines, paragraphs and block boxes pile up. In English both are the familiar left-to-right and top-to-bottom; in Arabic the inline direction runs right to left while blocks still stack downwards; in vertical Japanese the lines run top to bottom and stack from right to left.

This matters here for two reasons. Block boxes fill the inline size of their parent and stack in the block direction, whatever the language. And margin collapsing, below, happens only along the block axis, so in a vertical writing mode it is the left and right margins that merge. The flow-relative property names (margin-block, padding-inline, inline-size) simply name the sides of the box this way; when to use them across a product is the right-to-left topic's lesson.

The same two axes in three writing modes

Writing modeInline direction (a line runs)Block direction (blocks stack)Margin that merges with the next block
horizontal-tb, Englishleft to righttop to bottommargin-bottom
horizontal-tb, direction: rtl, Arabicright to lefttop to bottommargin-bottom
vertical-rl, Japanese set verticallytop to bottomright to leftmargin-left
3

How a block box gets its size

A block box in normal flow is measured against its containing block, which for a box in flow is the content box of its nearest block container ancestor. (Positioned boxes pick a different one; that is the positioning topic.) Across the inline axis the parts have to add up exactly: left margin, left border, left padding, width, right padding, right border and right margin together equal the containing block's width. Leave width as auto and it soaks up whatever is left, which is why a plain div fills its parent. Give it a width and set both inline margins to auto, and the two margins share the leftover equally: the box is centred.

Height works the other way round. An auto height is the height of whatever the box contains, so content decides it after the width is known. That is also why a percentage height often does nothing: if the parent's own height depends on its content, there is nothing fixed to take a percentage of, and the browser treats it as auto. Percentage widths always have something to measure against. A less obvious rule: percentage padding and margin are measured against the containing block's width on all four sides, top and bottom included, which is the old trick behind boxes that keep their shape (aspect-ratio does it directly today).

One Slatework column hands its cards 336 pixels

Assumptions
Column width
360 px
Column padding
0 12pxno padding at the top or bottom yet
Card padding and border
16 px and 1 px
Working
  1. The cards' containing block (the column's content box)360 − 12 − 12336 pxfrom Column width and Column padding
  2. Card with width: auto336 − 2 × 1 − 2 × 16302 px of content; the border box is exactly 336from The cards' containing block (the column's content box) and Card padding and border
  3. Card with width: 50% (content-box)50% × 336, then + 34168 px of content, 202 px border boxfrom The cards' containing block (the column's content box)
  4. padding: 5% on the card5% × 336, on every side16.8 px top, right, bottom and leftfrom The cards' containing block (the column's content box) · The top and bottom padding come from the width too, not from any height.
  5. height: 50% on the cardcolumn height is auto (it grows with its cards)treated as height autofrom Column width
  6. width: 280px; margin-inline: auto (border-box)(336 − 280) ÷ 228 px of margin on each sidefrom The cards' containing block (the column's content box)
What it means
  • A card that is told nothing about its width is the right width: it fills the 336 its column offers.
  • Percentages follow the parent's width, so changing the column's padding changes every percentage inside it.
4

Inline content and line boxes

Inside a block whose children are inline, the browser lays the content out in an inline formatting context. It fills a line box, a strip as wide as the block's content box, with words, links and chips one after another along the inline direction. When the next piece does not fit, it breaks at an allowed spot, usually a space, and starts a new line box under the first one. An inline element that does not fit is cut into pieces, one per line, each with its own share of the background. The block's height is then the line boxes stacked.

Inline boxes do not get a say in that height. width and height are ignored on a plain inline element such as a span. Horizontal padding, border and margin push neighbours along the line. Vertically, margins do nothing and padding and border only paint: the chip's background grows up and down and may overlap the line above, while the lines stay where line-height put them. When a piece of the line needs real height, make it inline-block (or inline-flex): it is then an atomic box that the line has to make room for.

One more surprise belongs here. A word with no break opportunity, such as a long URL pasted into a card title, cannot be split, so it overflows the line box sideways. overflow-wrap: anywhere lets the browser break it where it must.

A wrapped card title, as line boxes

The chip's 4 pixels of vertical padding paint above and below its line, overlapping line 2, but every line box is still 24 tall and the title still 72.

Inline chip. Box layout “h3.card-title”: 5 shapes. View: 318 × 140 from -16, -28. 1. “h3 block box” (rect) badge “line-height: 24px” 286 × 72 at 0, 0 blue 2. “line box 1” (rect) says “Fix flaky login test on the” 286 × 24 at 0, 0 sand in h3 block box 3. “line box 2” (rect) says “staging runner before the” 286 × 24 at 0, 24 sand in h3 block box 4. “line box 3” (rect) says “Friday release” 286 × 24 at 0, 48 sand in h3 block box 5. “span.tag” (rect) badge “inline, padding 4px” says “QA” 40 × 32 at 128, 44 orange in line box 3

What each outer type lets you set

Propertyblockinline (a span)inline-block
Starts on a new lineyesnono
Default inline sizefills the containing blockits contentits content (shrink to fit)
width and heightapplyignoredapply
Vertical padding and borderpush other boxespaint onlypush the line apart
Vertical marginspaces blocks, can collapseno effect on layoutspaces it in the line, never collapses
5

Margins that merge

Padding and border always add up. Vertical margins in block layout often do not. When two block-axis margins touch with nothing between them, the browser keeps one margin the size of the larger and throws the other away. The rule was made for text: a paragraph with 1em of space above and below should be 1em from the next paragraph, not 2em. It still applies to every block in normal flow, including cards, sections and headings, and it is the single most common reason a gap on screen does not match the numbers in the stylesheet.

Margins touch in three situations. Two siblings, one under the other: the bottom margin of the first meets the top margin of the second. A parent and its first child: if the parent has no top border, no top padding and no inline content before the child, the child's top margin and the parent's top margin are the same edge, and the merged margin ends up outside the parent. The same goes for a parent and its last child at the bottom, where a set height or min-height on the parent also keeps them apart. And an empty block: with no content, padding, border or height, its own top and bottom margins meet, and it can collapse through to join the margins on either side of it.

Negative margins join in by arithmetic. The result is the largest positive margin plus the most negative one; if every margin is negative, the most negative wins. Anything that puts something between the two edges stops the merge: a border, padding, a line of text. So does a parent that starts its own block formatting context (display: flow-root is the clean way; the next topic lists the others), and so does leaving normal flow altogether. Flex items and grid items never collapse, and neither do floats or absolutely positioned boxes. Inline-axis margins never collapse in any layout.

Two cards, two margins, one gap

Both margins start at A's bottom edge and overlap. The larger one wins, so B starts 24 below A, not 40.

Block column, gap 24. Box layout “div.column”: 5 shapes. View: 368 × 296 from -16, -28. 1. “div.column” (rect) badge “display: block” 336 × 240 at 0, 0 sand 2. “card A” (rect) badge “margin-bottom: 16px” says “Fix flaky login test” 336 × 96 at 0, 0 blue in div.column 3. “A's margin, 16” (rect) 160 × 16 at 0, 96 yellow in div.column 4. “B's margin, 24” (rect) 160 × 24 at 176, 96 yellow in div.column 5. “card B” (rect) badge “margin-top: 24px” says “Update the release notes” 336 × 96 at 0, 120 blue in div.column

  1. The column decides whether margins merge1BoardColumn

When block margins collapse, and what keeps them apart

CaseMargins that mergeKept apart by
Adjacent siblingsmargin-bottom of one + margin-top of the nextAnything in between that is not empty, clearance past a float, or the parent being flex or grid
Parent and first childparent margin-top + child margin-topParent's border-top or padding-top, inline content before the child, clearance, or the parent starting its own block formatting context
Parent and last childparent margin-bottom + child margin-bottomParent's border-bottom or padding-bottom, a set height or min-height, or its own block formatting context
Empty blockits own margin-top + margin-bottomAny content, padding, border, height or min-height
Neverinline-axis margins; floats; absolutely positioned boxes; flex and grid itemsNot applicable: these always add

Work these out before you open them

Q1
SiblingsOne card has margin-bottom: 16px, the next has margin-top: 24px, both in a plain block column. Why is the gap 24 and not 40?
The two margins touch with nothing between them, so they collapse into one margin the size of the larger. 16 disappears inside 24.
Q2
Empty blockBetween two paragraphs with 16 px margins sits an empty div with margin-top: 20px and margin-bottom: 30px. How far apart are the paragraphs?
30 px. The empty div has nothing to separate its own two margins, so they merge, and the result touches both paragraphs' margins too. Four margins become one: the largest, 30.
Q3
All negativeTwo touching margins are −10 px and −4 px. What is the result?
−10 px. When every margin involved is negative, the most negative one is used.
Q4
Padding insideThe cards have 16 px of padding. Why does that not stop their margins from merging with each other?
Padding separates a box's own margin from its children's margins. Between two siblings there is no padding; the cards' margins sit outside both border boxes and still touch.
Was this section helpful?

In practice.

Three bugs from the Slatework board, each traced to one rule from the section before, then how to bring the box model into an interview answer and where the system flows lean on it.

The Slatework board

Every column is the same width and hands its cards the same inline size; every card's height comes from its content.

As designed. Board: 7 items. Search: “Filter cards”. Count: 7 cards. Column “To do”: 1. Fix flaky login test · Due Fri by Ines R. badges: QA. 2. Update the release notes · Due Mon by Tomas K. Column “Doing”: 1. Dark mode for settings · Due Wed by Hana O. badges: Design. 2. Cache avatars on the board by Ines R. Column “Review”: 1. Retry failed uploads · Due Thu by Tomas K. 2. Rename the beta flag by Hana O. Column “Done”: 1. Ship the dark theme by Theo B. badges: Design.

  1. BoardColumn hands each card its width1BoardColumn
  2. TaskCard sizes to its content2TaskCard
1

Bug 1: the column's heading pushes the whole column down

Each column has a tinted background and side padding only. Its h2 has a 20 pixel top margin so the title does not touch the edge. On screen the tint starts 20 pixels lower than the other panels and the title sits flush against the top of the tint. The heading's margin has merged with the column's own top margin, because nothing (no padding, no border, no text) separates the two edges, and the merged margin is drawn outside the column, where the background does not reach.

The h2's margin ends up outside its column

The column's top edge and the heading's top margin touch, so they collapse into one 20 pixel margin outside the column. The tint starts 20 pixels low and the title sits on its top edge.

Margin escapes. Box layout “main.board”: 4 shapes. View: 416 × 308 from -16, -28. 1. “main.board” (rect) 384 × 264 at 0, 0 gray 2. “h2's 20px margin” (rect) 200 × 20 at 24, 24 yellow in main.board 3. “section.column” (rect) badge “padding: 0 12px” 360 × 200 at 12, 44 sand in main.board 4. “h2, margin-top: 20px” (rect) says “To do” 336 × 28 at 24, 44 purple in section.column

  1. BoardColumn1BoardColumn

The column, before and after

.column {
  background: var(--column-tint);
  padding: 0 12px;          /* no top padding: nothing between the edges */
}
.column > h2 {
  margin-top: 20px;         /* merges with .column's top margin */
}
.column {
  background: var(--column-tint);
  padding: 12px;            /* the margin now has something to push against */
}
.column > h2 {
  margin-top: 20px;         /* unchanged; it now stays inside the tint */
}
/* Or leave the padding alone and give .column display: flow-root */
2

Bug 2: a 320 pixel card that is 354 pixels wide

This is the opening bug. The card was given width: 320px, padding: 16px and a 1 pixel border under the default content-box sizing, so its border box is 354. The column is 360 wide with 12 pixels of padding on each side, which leaves 336 for the cards. The card sticks out 18 pixels past that content edge and 6 past the column itself, and in the right-most column that overflow can give the whole board a sideways scrollbar.

Switching the card to border-box makes the number in the stylesheet the number on screen: 320. The better fix is to stop telling the card its width at all. A block box with width auto fills its containing block exactly, so the card becomes 336 wide in this column and still fits if the column is resized, or the board is opened on a phone where columns are narrower.

The card against its column

The declared 320 is only the content. With padding and border the card is 354 wide and its right edge is at 366, past the column's 360.

content-box, 354. Box layout “section.column”: 2 shapes. View: 400 × 208 from -16, -28. 1. “section.column” (rect) badge “360 wide, 336 inside” 360 × 156 at 0, 0 sand 2. “article.card” (rect) badge “320 + 32 + 2 = 354” says “Fix flaky login test” 354 × 120 at 12, 12 orange in section.column

  1. TaskCard2TaskCard

Two lines that fix it for the whole app

*, *::before, *::after {
  box-sizing: border-box;
}
.card {
  /* no width: fill the column's content box */
  padding: 16px;
  border: 1px solid var(--card-border);
}
3

Bug 3: the gap between cards doubles after a refactor

Cards were spaced with margin-block: 12px, and in a plain block column the bottom margin of one card and the top margin of the next collapsed into a single 12 pixel gap. Then someone made the column a flex container, to pin an 'Add card' button to its bottom. Nothing about the cards changed, yet every gap became 24 pixels. Flex items never collapse margins, so each card's bottom margin and the next card's top margin now add up instead of merging.

The fix is to give the job of spacing to the column. In a flex or grid container, gap puts a fixed space between items and none before the first or after the last, so the spacing no longer depends on which layout mode the parent happens to use. (gap does nothing in plain block layout; there, a margin on the children or the owl-style selector .column > * + * is still the tool.)

Spacing owned by the child, then by the parent

.column { display: flex; flex-direction: column; }
.card   { margin-block: 12px; }  /* collapsed to 12 in block layout; adds to 24 here */
.column { display: flex; flex-direction: column; gap: 12px; }
.card   { margin: 0; }
4

Using it in an interview answer

What a strong answer says about layout, in a sentence each

State the baseline: a border-box reset, so a component's declared size is its real size.
Let containers own widths and spacing: columns from the grid, cards at width auto, gaps with gap.
Design for the longest realistic content: titles that wrap, URLs that cannot, names in other scripts.
Size with percentages and the parent's inline size rather than fixed pixels where the space varies.
Know what reruns layout: any change to a box's size or content can move every box after it in the flow.

Where the system flows lean on it

Notion-like block editor (notion/block-editor)
Every paragraph, heading and to-do is a block box stacked in normal flow. Spacing between blocks of different kinds is where collapsing margins bite; an editor that owns spacing in the parent gets the same rhythm whatever block sits next to which.
WhatsApp-like chat thread (whatsapp/chat-thread)
A message bubble is a box whose inline size is capped at a percentage of the thread's width and otherwise shrinks to its text, which wraps into line boxes. Long links and emoji runs are the overflow cases.
Facebook-like news feed (facebook/news-feed)
Feed cards fill a centred column whose width comes from its containing block. Reserving each card's media box before the image arrives is box sizing applied to layout shift.
Notion-like database views (notion/database-views)
Table cells with padding and borders only line up if every cell is sized the same way; a border-box reset keeps the column widths in the header and the body identical.
Was this section helpful?

Trade-offs.

The defaults of normal flow were designed for documents. An app like Slatework mostly overrides them, and each override has a cost worth naming.

01
Which box model the app uses
Chosen:border-box everywhere, via one reset
  • Pro:The size in the stylesheet is the size on screen, so design specs map one to one
  • Pro:Changing padding never changes a component's outer size or breaks a row of them
  • Pro:Percentages and padding mix safely (width 50% with padding stays half)
Downside we accept:
  • Con:Third-party widgets written for content-box can shift when the reset reaches them
  • Con:Content size is now derived, and is floored at zero when padding plus border exceed the width
Ruled out:content-box, the initial value

Every width needs padding and border added in your head; width: 100% plus any padding overflows the parent

02
Who owns the space between items
Chosen:The parent, with gap (flex or grid)
  • Pro:One number, in one place, between items only, never before the first or after the last
  • Pro:Unaffected by margin collapsing, so it reads the same in every layout mode that supports it
  • Pro:Children stay reusable: a card carries no outside spacing into the next context
Downside we accept:
  • Con:Needs the parent to be a flex or grid container; plain block layout ignores gap
  • Con:Uneven spacing (more room before a section heading) needs an extra rule anyway
Ruled out:Margins on the children

The same margins give different gaps in block and flex parents; First and last children leak margin through a parent with no padding or border; Every reusable component brings outside spacing that the next layout has to cancel

03
Where a card's width comes from
Chosen:The container (width auto, max-inline-size where needed)
  • Pro:Fills any column it is dropped into, on desktop or a phone
  • Pro:Nothing has to add up across components, so nothing overflows when one value changes
Downside we accept:
  • Con:The card's final width is not visible from its own stylesheet
  • Con:Very wide containers need a cap to keep lines readable
Ruled out:A fixed width on the card

Has to agree with every container's padding and the box-sizing in force; Breaks the first time a column, a sidebar or the viewport gets narrower

Things people believe about boxes, and what is actually true

BeliefWhat actually happens
width: 100% means the box fills its parentOnly under border-box and with no inline margins. Under content-box the padding and border are added on top and it overflows. width auto fills without any of that.
Margins always add upThey add along the inline axis, between flex or grid items, and wherever padding or a border separates them. Block-axis margins that touch in normal flow merge.
Padding-top: 10% is 10% of the heightPercentage padding and margin are taken from the containing block's width on every side.
height: 100% makes a child as tall as its parentOnly if the parent has a definite height. If the parent's height comes from its content, the percentage acts as auto.
A span with padding makes its line tallerVertical padding on an inline box only paints. Make it inline-block to give it height in the line.
This topic
Which boxes display generates, outer and inner types
The four areas and box-sizing arithmetic
Block and inline directions, line boxes, percentages
Margin collapsing and what prevents it
Elsewhere
What creates a block formatting context, flex and grid sizing, min-width autoFormatting contexts
Containing blocks for positioned boxes, sticky, fixed, clippingPositioning and containing blocks
Who paints on top, z-index, the top layerStacking contexts and the top layer
Logical properties across a product, right-to-left mirroringRight-to-left and text layout
The cost of re-running layoutLayout thrashing
Was this section helpful?
Builds on this
Formatting contexts
Read next