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 (block or inline) 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:

  1. It contains the floats inside it. A float can't escape, so the parent's height includes it.
  2. Its margins don't collapse with its children's. What you set inside stays inside.
  3. 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),
  • overflow set to anything other than visible — the old overflow: hidden trick,
  • floated and absolutely positioned elements,
  • flex and grid items,
  • table cells,
  • contain: layout, content or paint,
  • 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 writing overflow: hidden without wanting to clip anything, what you actually wanted was flow-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:

  1. display: flow-root on the parent — the cleanest,
  2. padding or a border between parent and child — even a hairline is enough,
  3. make the parent a flex or grid container — item margins never collapse,
  4. 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 inline outside,
  • 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: block or inline,
  • a float hanging out of its container? → display: flow-root,
  • a margin escaping the parent? → display: flow-root on the parent,
  • about to write overflow: hidden but 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.