display is the most-used property in CSS, and probably the least understood one.
We know flex makes a flexbox, inline-block is somehow "both at once",
and block puts things on their own line — but why they behave that way is
something most of us never actually learned.
There is a simple, consistent system underneath. Every box has two display types: one facing outward and one facing inward. Once that clicks, a pile of seemingly unrelated mysteries resolves at the same time — from margin collapsing to clearfix.
This guide walks through that model with practical recipes: what the outer and inner types are,
what a block formatting context is, what flow-root is for, and what you were
really writing every time you typed inline-block.
Every box has two display types
Let's start with the point. When you write this:
.card {
display: flex;
}
what the browser actually reads is this:
.card {
display: block flex;
}
That's not a typo, and it isn't a new feature: display has always set two
things at once. It's just that for decades both were compressed into a single keyword, and the
second one went unnoticed.
- the outer type (
blockorinline) describes how the box behaves in its parent's layout, - the inner type (
flow,flow-root,flex,grid,table,ruby) describes the rules it uses to lay out its children.
The two-value syntax adds no new capability — it just says out loud what was already happening. That is exactly what makes it such a good teaching device: you read it instead of memorising it.
display isn't one setting, it's two: what am I from the outside, and what happens
inside.
The mapping table
This is the most important table in the article. Every familiar display value
corresponds to exactly one outer + inner pair:
| What you write | What it means | Outside / inside |
|---|---|---|
block |
block flow |
block-level / normal flow |
inline |
inline flow |
inline-level / normal flow |
inline-block |
inline flow-root |
inline-level / own BFC |
flow-root |
block flow-root |
block-level / own BFC |
flex |
block flex |
block-level / flex |
inline-flex |
inline flex |
inline-level / flex |
grid |
block grid |
block-level / grid |
inline-grid |
inline grid |
inline-level / grid |
table |
block table |
block-level / table |
inline-table |
inline table |
inline-level / table |
list-item |
block flow list-item |
block-level / flow + marker |
Look at that list for a moment and the system becomes visible: the inline- prefix
only ever swaps the outer type, leaving the inner layout untouched. flex
and inline-flex are exactly the same flexbox on the inside — one takes its own
line, the other stays in the text flow.
If you only write one half: the browser fills in the other. Give only an
inner value (display: flex) and the outer defaults to block. Give
only an outer one (display: block) and the inner defaults to flow.
That is precisely why the old and new spellings produce identical results.
The outer type: how it behaves outward
In practice the outer type has two values: block and inline. This is
the box's "social behaviour" — what its parent does with it.
block— the box takes its own line and fills the available width by default, with a break above and below it.inline— the box joins the text flow, can break across lines, and takes only as much room as its content needs.
The important part is that the outer type says nothing about what happens inside the
box. An inline box can contain a full grid layout — it will still sit in the
middle of a sentence.
There is a third outer value in the specification called run-in, but nobody
implemented it in practice. You can safely forget it exists.
The inner type: how it lays out its children
The inner type is the box's internal affair: which layout rules it uses to arrange its children. There are six values:
flow— normal flow: blocks stacked vertically, inline content wrapped into lines. This is the default.flow-root— the same normal flow, except the box establishes a new block formatting context. More on that shortly.flex— children become flex items, arranged along one axis.grid— children become grid items, arranged in rows and columns.table— table layout, without needing an HTML<table>.ruby— layout for East Asian pronunciation annotations; you'll rarely meet it.
Here is where the model earns its keep. When you put display: grid on an element,
you are not changing how that element behaves — you are changing how its children
behave. The element itself remains the same block-level box it always was. Notice that once,
and a lot of surprises stop being surprising.
The same idea carries into modern layout tooling: if you want components to respond to their own space rather than the screen's, that's what our article on container queries is about.
Why this matters in practice
Three classic misunderstandings that this model resolves in one sentence each.
"I made it a flexbox and it's still on its own line"
Because display: flex sets the inner type to flex. The outer type stayed
block, so the container still takes its own line and fills the width. If you want
a flex container that stays in the text flow, it's the outer type you need to change:
.chip {
display: inline-flex; /* = inline flex */
}
"My inline element ignores its padding"
Vertical padding and margin on an inline flow box don't affect line height — which
is why it spills over the neighbouring lines. What you want is an inline-level box that is a
proper block environment inside: that is inline flow-root, better known as
inline-block.
"Why don't margins collapse inside flex?"
Because margin collapsing is a rule of normal flow (flow). Flex and grid items
aren't in normal flow, so the rule simply doesn't apply to them. It isn't an exception — it's a
different layout model.
flow-root: the missing piece
flow-root is the one value in the family many people never learned — and it's the
most useful. Its name describes exactly what it does: normal flow, with this box as
its root.
.wrapper {
display: flow-root; /* = block flow-root */
}
From the outside it's a perfectly ordinary block-level box. Inside, it opens a new self-contained layout world that things don't leak out of, and that outside things don't leak into. That world is the block formatting context.
This one you can ship today. flow-root support is around 96%
(Chrome 58, Firefox 53, Safari 13, Edge 79) — safe for years now. Don't confuse it with
support for the two-value syntax, which is lower.
What is a block formatting context?
A block formatting context (BFC) is an isolated layout region. Anything that establishes one gets three properties:
- It contains the floats inside it. A float can't escape, so the parent's height includes it.
- Its margins don't collapse with its children's. What you set inside stays inside.
- It doesn't overlap floats from outside. Instead of flowing underneath one, it sits beside it.
There are several ways to establish one. In practice you'll meet these:
display: flow-root— the deliberate, side-effect-free way,display: inline-block(that is,inline flow-root),overflowset to anything other thanvisible— the oldoverflow: hiddentrick,- floated and absolutely positioned elements,
- flex and grid items,
- table cells,
contain: layout,contentorpaint,- and the root element itself,
<html>.
For years overflow: hidden was the standard move, but it has an unpleasant side
effect: it genuinely clips overflowing content. A focus ring, a tooltip or a negatively
margined element can easily fall victim to it. flow-root exists precisely so you
can ask for a BFC without the side effect.
If you've been writingoverflow: hiddenwithout wanting to clip anything, what you actually wanted wasflow-root.
Margin collapsing
Margin collapsing is the rule that makes two adjacent vertical margins merge into the larger of the two rather than adding up. It belongs to normal flow, and it shows up in three places:
- between adjacent siblings — one's bottom margin merges with the next one's top margin,
- between a parent and its first child's top margin — the child's margin "escapes" the parent,
- between a parent and its last child's bottom margin — the same thing downward.
The second case causes the most common mystery: you give a heading inside a card a top margin, and the margin appears above the card instead, with the card's background not reaching up to it. That isn't a bug — by the rules, the margin escaped.
There are four ways to stop it:
display: flow-rooton the parent — the cleanest,- padding or a border between parent and child — even a hairline is enough,
- make the parent a flex or grid container — item margins never collapse,
- make the child absolutely positioned or floated.
The second one works because with even 1px of padding in between, the two margins
no longer touch, so there's nothing to collapse. Plenty of people use it, but
flow-root is more honest, because it doesn't change the box's size.
Recipes
Four recurring situations where this model pays off immediately.
1. Containing floats without a clearfix
If a container holds only floated elements, its height collapses to zero — the floats hang out of it. For years, this was the answer:
/* The old clearfix */
.wrapper::after {
content: "";
display: table;
clear: both;
}
Today it's one line, with no pseudo-element:
.wrapper {
display: flow-root;
}
Same result, except it states the intent: "this box should contain its own contents."
2. Killing the mysterious gap above a card
The classic case: the card's background doesn't start where you expect, because the first child's top margin escaped.
.card {
background: var(--surface);
border-radius: 12px;
display: flow-root; /* the child's margin stays inside */
}
.card h3 {
margin-top: 1.5rem; /* now measured within the card */
}
The advantage of flow-root over padding is that it doesn't push the content
inward — you're still free to set the inner margins however you like.
3. Text that doesn't wrap around an image
By default, text wraps around a floated image. If you'd rather have a clean two-column effect, turn the text block into a BFC — it won't overlap the float, it'll sit beside it:
.figure {
float: left;
width: 12rem;
margin-right: 1.5rem;
}
.body {
display: flow-root; /* its own column, no wrapping */
}
4. A label that can carry padding
An element that stays on the line but behaves like a box — a badge, a button, a tag — is
exactly the inline flow-root case:
.badge {
display: inline-block; /* = inline flow-root */
padding: 0.2em 0.6em;
border-radius: 999px;
background: rgba(0,0,0,0.06);
}
With plain inline, the vertical padding wouldn't push the lines apart — it would
spill over the neighbouring ones. The flow-root inner type is what makes it a real
box.
inline-block: what you were really writing
inline-block may be the worst-named value in CSS. The name suggests "inline and
block at the same time", which is meaningless on its face, since the two are mutually
exclusive.
The two-value form clears it up instantly:
display: inline flow-root;
Inline-level on the outside, its own block formatting context inside. That explains every behaviour we usually memorise separately:
- why it stays in the text flow → because it's
inlineoutside, - why it accepts width and height → because inside it's a proper block environment,
- why it contains its floats → because it's a BFC,
- why its margins don't collapse with its children's → also because it's a BFC.
One sentence instead of four "rules". That's the real value of the two-value syntax, even if you never type it.
list-item: the three-value oddity
list-item sticks out of the system, because it isn't an outer or inner type but a
third, independent component: it says whether the box gets a ::marker. So its
full form is three words long:
display: block flow list-item; /* = display: list-item */
And because it's an independent component, it can be combined. An inline-level list item, for instance, looks like this:
display: inline flow list-item;
You'll rarely need it in practice, but it shows the system is consistent: whether there's a marker and how the box lays out are two separate questions.
What has no two-value form
Not every display value fits the model — and that isn't a gap, it's logical. Those
values simply aren't layout modes.
Box generation: none and contents
display: none says the element should not generate a box at all. Where there is no
box, asking about its outer or inner type is a meaningless question.
display: contents is more interesting: the element's own box disappears, but its
children remain and behave as if they were direct children of the grandparent. It's an
excellent tool when a redundant wrapper is blocking a grid or flex layout:
.grid { display: grid; grid-template-columns: repeat(3, 1fr); }
/* The wrapper's box disappears; its children become grid items */
.grid > .wrapper { display: contents; }
Handle contents with care. For a long time it had a serious
accessibility bug: the affected element's semantics vanished from the accessibility tree, so
a <ul> stopped existing as a list for screen readers. Modern engines have
largely fixed this, but on semantic elements — lists, tables, buttons — it's still worth
being cautious and verifying with a screen reader. On a plain <div>
wrapper it's perfectly safe.
Internal table values
table-row, table-cell, table-header-group and their
relatives also stay single-valued. They aren't standalone boxes in the usual sense but
internal structural roles within a table — they only mean anything inside a table
layout.
Should I ship the two-value syntax?
Honest answer: not yet — but not because there's anything wrong with it.
The behaviour of the two spellings is identical, so the longer one gains you nothing. On top of that, support sits at around 93%, which means you'd need a fallback line too:
.badge {
display: inline-block; /* fallback */
display: inline flow-root; /* the same thing, spelled out */
}
Two lines, zero benefit. This is where the topic differs fundamentally from, say, the dynamic viewport units: those solved a real bug, whereas this describes the same thing differently.
Where it does earn its place:
- teaching and code review — a
/* inline flow-root */comment explains more than a paragraph, - thinking — when a layout misbehaves, ask yourself: what's its outer type, and what's its inner type?
- documentation — in a design system write-up it's more precise than the legacy names.
As support climbs past 98% this will probably flip: inline flow-root explains
itself, while inline-block will always be a misleading name.
Browser support
It's important to separate two different things here, because their situations differ sharply.
| What | Since | Coverage |
|---|---|---|
flow-root |
Chrome 58, Firefox 53, Safari 13, Edge 79 | ~96% — use it |
| two-value syntax | Chrome 115, Edge 115, Firefox 70, Safari 15 | ~93% — still wait |
In other words, the idea is fully usable today — flow-root, BFCs and the
outer/inner model are all fair game. It's only the syntax that hasn't caught up yet.
Quick decision aid
When a layout has you stuck, this list usually gets you out:
- want to change how the children are arranged? → inner type:
flex,grid,flow-root, - want to change how the box itself sits among its siblings? → outer type:
blockorinline, - a float hanging out of its container? →
display: flow-root, - a margin escaping the parent? →
display: flow-rooton the parent, - about to write
overflow: hiddenbut don't want to clip anything? →flow-root, - should stay on the line but needs padding? →
inline-block, - need a flex container that stays in the text? →
inline-flex, - a redundant wrapper blocking your grid? →
display: contents, with an accessibility check, - in production code, keep writing the single-value forms for now.
Frequently asked questions
What is the two-value display syntax?
display has always set two things at once: a box's outer type (how it behaves among
its siblings) and its inner type (how it lays out its children). The two-value syntax simply
makes that visible: display: flex is exactly the same as
display: block flex, and inline-block is shorthand for
inline flow-root.
What is the difference between outer and inner display type?
The outer type (block or inline) describes how the box itself behaves
in its parent's layout: whether it takes its own line or stays in the text flow. The inner type
(flow, flow-root, flex, grid,
table, ruby) describes the layout rules it uses to arrange its own
children. The two are independent of each other.
What is flow-root and what is it for?
display: flow-root creates a block-level box that establishes a new block
formatting context for its contents. In practice it is the modern clearfix: it contains any
floats inside it and stops its children's margins from escaping. Browser support is excellent
at around 96%, so it's safe to use in production today.
What is the difference between inline-block and inline flow-root?
Nothing — they're two spellings of the same thing. inline-block is the old
single-keyword name for what the two-value syntax writes as inline flow-root: an
inline-level box on the outside with its own block formatting context inside. The longer form
just states more precisely what's actually happening.
How do I stop margin collapsing?
The cleanest way is to establish a new block formatting context on the parent with
display: flow-root. Padding or a border between parent and child also prevents it,
as does making the parent a flex or grid container — flex and grid items never collapse their
margins.
Can I use two-value display in production yet?
Technically yes, but there's little point right now. Support is around 93% (Chrome and Edge 115, Firefox 70, Safari 15), so you'd need a fallback line alongside it — and that fallback is longer than the single-value form it falls back to, while the behaviour is identical. Use the model to think with, and write the single-value forms.
Why is there no two-value form of display: none?
Because none isn't a layout mode but a box-generation setting: it says the element
should not generate a box at all. Where there's no box, asking about its outer or inner layout
type is meaningless. The same applies to display: contents and to internal table
values such as table-cell.
Is display: contents safe to use?
It's useful for layout, because it removes a redundant wrapper's box so its children participate directly in the grandparent's grid or flex layout. For a long time it had a serious accessibility bug, though: the affected element's semantics disappeared from the accessibility tree. Modern engines have largely fixed this, but on semantic elements — lists, tables, buttons — it still deserves caution and a screen reader check.