CSS takes a source document organized as a tree of elements… and text nodes… and renders it onto a canvas such as your screen.
CSS Layout starts with a source document (usually HTML), which is organized into a nested ‘tree’ of elements & text nodes.

This is the DOM tree, what we see when we open the web inspector. It’s based on our HTML, but might not match exactly, because it goes through a cleanup step. Here I’m hovering over a text node in the inspector, and it’s highlighted on the page.
The document canvas is the infinite surface over which the document is rendered.
That document is rendered onto an infinite canvas.

But we interact with the document through our finite viewport. And that’s not really like viewing space through a window on the international space station, is it?

Because the content from our document will move and re-size and try to fit inside whatever window we’re using!
Our viewport creates an ‘initial containing block’ which the document is placed into – anchored at one corner, and potentially overflowing at the opposite corner.

CSS layout is defined by this back-and-forth struggle between external context – how much extrinsic space is available – and internal content that needs a certain amount of space to render. It has some intrinsic size.
Context pushing in, and content pushing out – sometimes overflowing the container.
Again, this awesome meme nails the fundamental problem of CSS. We have a limited viewport, and a whole document to fit inside it. How are we going to negotiate a solution that makes everything on the page accessible in the broad sense – something we can all access.
And like everything else in CSS, there’s a default setup in place – designed to be as flexible as possible.
The default layout model is called flow or sometimes normal flow. But grid and flexbox and other layout modes also create a sort of document ‘flow’.

The flow describes how our elements and text nodes share space in the container by default – starting in one corner and stacking in rows or columns or both. Each element taking up space, and pushing the elements next to it.
Text stacks up in lines on the inline axis. For English text that’s horizontal, with lines flowing left to right.
direction
[flow-inline-rtl: flow-inline-rtl]But that depends on the language direction. A right-to-left language (like Arabic) is still written in horizontal lines, but the direction is reversed (right to left).
writing mode
[flow-inline-vert: flow-inline-vert]And some languages are written in vertical lines, generally top to bottom. We call this the writing mode.
When a line of text reaches the edge of the container, it fragments to create new lines – which stack together on the block axis.
For English this is vertical, with blocks stacking top-to-bottom.
So in addition to our physical dimensions (width and height, which are fixed in place) we can talk about –
The ‘flow-relative’ or ‘logical’ dimensions, which are based on the writing mode.
We also get four logical ‘sides’. Top, right, bottom, and left are fixed in place – but block-start, block-end, inline-start, and inline-end orient us based on writing mode and direction.
The start or end of a line, and the start or end of a block.
Most web design is built around text, so we’re often making decisions relative to the flow. English sites often put the logo top left because that’s the start of our document flow – left to right, top to bottom.
There might be some exceptions. Mobile sites will sometimes put controls at the bottom of the page – not because of the document flow, but because that’s easier to reach when you’re holding a phone.
Our job, as always, is to express what we mean as clearly as possible. The logical properties help us do that.
By using flow-relative properties our sites can re-orient efficiently without other changes. That’s useful even if we never translate our own site, because some browsers might translate for us!
As a bonus, we get some useful shorthands for margin, padding, or borders on the inline or block axis. I use these all the time!
Normal flow is designed to be invisible and obvious as a layout mode. It’s just the way we write documents.
There’s a great talk by the creators of CSS (Håkon Lie & Bert Bos) where they say “We didn’t think we needed to specify this,” because “everyone knows how a document works”. But let’s talk about how it works.
Because there are actually two different layout modes that are part of normal flow.
Inline flow is made out of text nodes and boxes that flow in line with that text – and fragment at the edge of the container, wrapping to a new line.
Inline boxes get their inline size from contents, and their block size from the container – from the height of the line. If we add content, the inline size grows to contain it, even if that means wrapping.
We’re not allowed to set an explicit size on inline elements. We can add border/padding/margins, but they only impact the inline-flow – they don’t change the size of the line itself.
Block flow requires everything to stack on the block axis. There’s no wrapping at a container edge, there’s just the stack.
Block boxes get their block size from contents, and their inline size from the container. If we add more contents, the block-size will grow to contain it.
Block boxes can be given an explicit size, but even making them narrow doesn’t change the stack. Boxes don’t move up to fit in empty spaces.
Block and inline flow don’t really mix. Which one we get is determined by what content we have. If all the siblings are text or inline elements, we get inline flow. But as soon as we put a block in there, browsers split the inline parts into ‘anonymous’ boxes on either side.
Including % and other ‘relative’ units
It doesn’t matter what units we’re using.
This includes % and em and vw and other relative units.
While they are relative to other aspects of the design, they are still imposed from outside. But we do have a few…
Boxes don’t just have one intrinsic size, they often have a minimum, and maximum – depending on how content is wrapped. The auto behavior for a block box is the container size, clamped at the low end by the min-content size.
We have keywords to choose
one of these intrinsic sizes,
or change the clamping between min and max with fit-content.
Replaced elements like images and video
can also have a ‘natural size’
that comes from the source file.
min-content | max-content | fit-content
display…
inline or block
We’re all familiar with these two display modes, inline and block, right?
These are called ‘outer’ display modes. They change how the outside of the item behaves in a normal flow context.
As soon as the element is removed from normal flow,
there’s no difference.
Take a span or div
and put it anywhere else –
grid, flexbox, absolute positioning, even floats –
and they’ll act identical.
Grid items aren’t inline or block, they’re just grid items. The outer display is ignored.
display…
Inner Layout Mode
‘normal’ flow, multi-column flow, grid, flexbox, tables, etc.
But the display property can also be used to control the inner layout mode of an element – the formatting context that is used to lay out children.
display:
flow | flow-root
That includes the options to explicitly create
a flow or flow-root context.
display-flow-root
These are kinda new, but they’ve been Baseline since 2020.
block ➡️ block flow inline ➡️ inline flow
The reason you rarely see them
is because they are the default
inner display mode.
If you set display: block or display: inline,
that implies you want more flow
inside the element.
It’s the same as writing display: block flow
or display: inline flow.
Two Value Display
That’s right, we can set two values for the display property! But usually we don’t have to, because the implied default is correct.
flow ➡️ block flow grid ➡️ block grid
It works the other way too. If we only set an inner display mode, the outer default is block. If we ask for flow inside, we get a block box to contain that flow. If we ask for grid or flex or something else inside, we also get a block box to contain it.
inline-* / inline *
But sometimes we do need a special combo,
where we get an inline box
with something different inside!
That’s why we have options like
inline-block, inline-grid, and inline-flex.
inline-flex ➡️ inline flex inline-grid ➡️ inline grid
It’s fine to use the one-word hyphenated versions of those, but the Working Group recommends moving away from that syntax, and using two words instead.
There’s an active debate arguing if new layouts should even get the one-word option.
inline-block ➡️ inline flow-root
The strange case here is the oldest one. With inline-block, what we want is not just an inline-flow (that’s the default) – we need an inline flow root.
While this is still behaving like an inline element externally, sitting on a line of text, we can give it explicit sizes, and margins or padding or borders that impact the line.
flow-root contains…
“A mini layout in your layout”
Flow-root is not just creating a flow context, but isolating that flow from outside interactions. It’s “a mini layout inside your layout”.

Technically this is called a
Block Formatting Context.
Or what IE6 used to call hasLayout,
if you remember that.
It’s the same thing we need to create
for a clearfix
to wrap around floated elements.
Do the kids these days still know about the clearfix?
Well it’s a lot easier now.
Display flow-root is all you need.
display property
Box Generation Values
There are a few more things
the display property can do.
It really is overloaded,
and I think we should treat it as a shorthand.
In addition to inner and outer display values, we can also remove the element box entirely, or generate extra boxes.
(by default)
By default every element on the page generates a single box.
display: none
Removes Box Tree
Display none will remove the box and everything inside it. Nothing is rendered.
display: contents
Removes Box
Display contents removes the box, but leaves everything inside. This can be helpful in some situations, but we have to be careful…
This can cause major accessibility issues if we’re not testing. Much of that has been fixed, but not everyone is on the latest browsers, and…
Browsers still have issues on HTML buttons and tables. So never use display contents on those elements.
I’d also be cautious with anything interactive, or where the semantics matter. I mostly use this for removing unnecessary wrapper divs, that come from a JS framework or somewhere else.
display: list-item
Adds Marker Box
Finally,
the list-item display
generates an additional ::marker box –
which we can move in or out of the principal box
using list-style-position.
We can style that box to some extent,
using the ::marker pseudo-element.
box-sizing property…
Selects Box Edge for Sizing
But let’s get back to layout,
and a very useful little property –
box-sizing –
which I think is over-used
and under-used at the same time.

If we’re setting an explicit size on something, we need to decide what size we care about – the outside edge of the border, or the outside-edge of the contents.

content-box modelI think the default here –
content-box sizing –
makes a lot of sense
if we’re setting up a container,
and we care about what fits inside.
If we add padding and border to that, we’re not taking anything away from the content – it just makes the container larger.

border-box modelBut if we’re trying to lay items out on a grid,
then border-box sizing makes more sense.
What we care about is how the borders line up,
so we set the outside of the box,
then subtract padding & borders
to get the available content size.
Inside or outside dimensions?
Neither of those is better all around, it just depends if you’re worried about the inside dimensions of the box, or the outside dimensions.
* { box-sizing: border-box; }
But it used to matter! Because we were setting sizes on everything to create a grid out of floated elements. So people got in a habit of setting it globally as part of the reset styles.
Only set it when it matters…
That’s fine, but I think most sites could remove it now, without even noticing. I tried pulling it out of some large projects, and basically nothing broke.
With flexbox and grid, we’re just not setting explicit sizes on everything.
.container {
inline-size: calc(34em + (var(--padding) * 2));
}
content-box sizing instead…The clue that you need to change models
is any time you find yourself
adding or subtracting the padding and borders
from a width.
If you’re adding them to get an outer width,
use content-box –
.container {
inline-size: calc(34em - (var(--padding) * 2));
}
border-box sizing instead…And if you’re removing them
to get an inner width,
use border-box instead.