https://www.joshwcomeau.com/css/understanding-layout-algorithms/
JoshWComeau
* Latest
* Posts
* Snippets
* Goodies
CSS
Understanding Layout Algorithms
The mental model shift that makes CSS more intuitive
A few years ago, I had a Eureka! moment with CSS.
Up until that moment, I had been learning CSS by focusing on the
properties and values we write, things like z-index: 10 or
justify-content: center. I figured that if I understood broadly what
each property did, I'd have a deep understanding of the language as a
whole.
The key realization I had is that CSS is so much more than a
collection of properties. It's a constellation of inter-connected
layout algorithms. Each algorithm is a complex system with its own
rules and secret mechanisms.
It's not enough to learn what specific properties do. We need to
learn how the layout algorithms work, and how they use the properties
we provide to them.
Have you ever had the unsettling experience of writing a familiar
chunk of CSS, something you've used many times before, only to get a
different and unexpected result? It's super frustrating. It makes the
language feel inconsistent and flaky. How can the exact same CSS
input produce a different output??
This happens because the properties are acting on a complex system,
and there's some subtle context that changes how the properties
behave. Our mental model is incomplete, and it leads to surprises!
When I started digging into the layout algorithms, everything started
to make more sense. Mysteries that had bothered me for years were
solved. I realized that CSS is actually a pretty darn robust
language, and I started to really enjoy writing it!
In this blog post, we'll look at how this new lens can help us make
sense of what's happening in CSS. And we'll use that lens to solve a
surprisingly-common mystery. [?]
Link to this heading
Layout algorithms
So, what is a "layout algorithm"? You're probably already familiar
with some of them. They include:
* Flexbox
* Positioned (eg. position: absolute)
* Grid
* Table
* Flow
(Technically, they're called layout modes, not layout algorithms. But
I find "layout algorithm" to be a more helpful label.)
As the browser renders our HTML, every element will have its layout
calculated using a primary layout algorithm. We can opt into
different layout algorithms with specific CSS declarations. For
example, applying position: absolute will switch an element to use
Positioned layout.
Let's look at an example. Say I have the following CSS:
css
Our first order of business is to figure out which layout algorithm
will be used to render the .box element. Based on the CSS provided,
it will be rendered using Flow layout.
Flow is the "OG" layout algorithm of the web. It was created in an
era when the web was primarily seen as a giant hyperlinked set of
documents, like the world's largest archive. It's similar to the
layout algorithm used in word-processing software like Microsoft
Word.
Flow is the default layout algorithm used for non-table HTML
elements. Unless we explicitly opt in to another layout algorithm,
Flow will be used.
The z-index property is used to control stacking order, to figure out
which one shows up "on top" if they overlap. But here's the thing:
it's not implemented in Flow layout. Flow is all about creating
document-style layouts, and I have yet to see word-processing
software that allows elements to overlap.*
If you had asked me about this a few years ago, I would have said
something like:
You can't use z-index without also setting position to something
like "relative" or "absolute", because the z-index property
depends on the position property.
This isn't exactly wrong, but it's a subtle misunderstanding. It's
more accurate to say that the z-index property is not implemented in
the Flow layout algorithm, and so we'd need to pick a different
layout algorithm if we want this property to have an effect.
This might seem like I'm being pedantic, but this small
misunderstanding can lead to big confusion. For example, consider
this:
Code Playground
Format code using Prettier
Reset code
HTMLCSS
[
Result
Refresh results pane
[ ]
Enable 'tab' key
In this demo, we have 3 siblings arranged using the Flexbox layout
algorithm.
The middle sibling sets z-index, and it works. Try removing it, and
notice that it falls behind its sibling.
How can this be? We haven't set position: relative anywhere!
This works because the Flexbox algorithm implements the z-index
property. When the language authors were designing the Flexbox
algorithm, they decided to wire up the z-index property to control
stacking order, just like it does in Positioned layout.
This is the critical mental-model shift. CSS properties on their own
are meaningless. It's up to the layout algorithm to define what they
do, how they're used in the calculations.
To be clear, there are some CSS properties that work the same in all
layout algorithms. color: red will produce red text no matter what.
But each layout algorithm can override the default behavior for any
property. And many properties don't have any default behavior.
Here's an example which blew my mind: Did you know that the width
property is implemented differently depending on the layout
algorithm?
Here's proof:
Code Playground
Format code using Prettier
Reset code
HTMLCSS
[
Result
Refresh results pane
[ ]
Enable 'tab' key
Our .item element has a single CSS property: width: 2000px.
The first instance of .item is rendered using Flow layout, and it
will actually consume 2000px of width. In Flow layout, width is a
hard rule. It will take up 2000px of space, consequences be damned.
The second instance of .item, however, is rendered inside a Flex
container, which means it's using Flexbox layout. In the Flexbox
algorithm, width is more of a suggestion.
The Flexbox specification calls this the hypothetical size. It's the
size that the element would be, in an idyllic world, with no
constraints or forces acting upon it. In a perfect world, this item
would be 2000px wide, but it's been placed in a narrower container,
and so it'll shrink to accommodate it.
Once again, the framing is super important here. It's not that width
has some special caveat when it comes to Flexbox. It's that the
Flexbox algorithm implements the width property in a different way
than the Flow algorithm.*
The properties we write are inputs, like arguments being passed to a
function. It's up to the layout algorithm to choose what to do with
those inputs. If we want to understand CSS, we need to understand how
the layout algorithms work. Knowing the properties alone is
insufficient.
Link to this heading
Identifying the layout algorithm
CSS doesn't have a layout-mode property. There are several properties
that can tweak the layout algorithm used, and it can actually get
pretty tricky!
In some cases, a CSS property applied to an element will opt in to a
specific layout mode. For example:
css
In other cases, we need to look at what CSS the parent applies. For
example:
html
When we apply display: flex, we aren't actually using the Flexbox
layout algorithm for the .row element; instead, we're saying that its
children should be positioned using Flexbox layout.
In technical terms, display: flex creates a flex formatting context.
All direct children will participate in this context, and it means
that they will use Flexbox layout instead of the default Flow layout.
(display: flex will also turn an inline element, like a into a
Block-level element, so it does have some effect on the parent
element's layout. But it won't change the layout algorithm used.)
Link to this heading
Layout algorithm variants
Some layout algorithms are split into multiple variants.
For example, when we use Positioned layout, that refers to several
different "positioning schemes":
* Relative
* Absolute
* Fixed
* Sticky
Each variant is sorta like its own mini-layout algorithm, though they
do share things in common (eg. they can all use the z-index
property).
Similarly, in Flow layout, elements can either be block or inline.
We'll talk more about Flow layout shortly.
Link to this heading
Conflicts
What happens when multiple layout algorithms are applied to an
element?
For example:
html
All three list items are children inside a Flex container, so they
ought to be positioned according to Flexbox. But that middle child
opts into Positioned layout, by setting position: absolute.
As I understand it, an element will be rendered using a primary
layout mode. It's a bit like specificity: certain layout modes have
higher priority than others.
I don't know the exact hierarchy, but Positioned layout tends to beat
everything. And so, in this example, the middle child will use
Positioned layout, not Flexbox.
As a result, the Flexbox calculations will act as though there are
only two children, not three. As far as the Flexbox algorithm is
concerned, that middle child doesn't exist. It has no impact on the
algorithm at all.
In general, conflicts are usually pretty obvious / intentional. But
if you ever find that an element isn't behaving the way you'd expect,
it can be worth trying to identify which layout algorithm it's using.
The answer might surprise you!
Relative positioning
So, here's a conundrum: if each element is rendered using a single
layout algorithm, how do I explain relative positioning?
An element with position: relative is clearly rendered using
Positioned layout. It can use exclusive Positioned-layout properties
like top or left. And yet, it can also participate in Flexbox / Grid
layouts!
We're starting to veer outside the scope of this article. This stuff
gets complicated! But here's a quick explanation, for those curious:
Show more
Link to this heading
Inline magic space
Alright, let's look at a classic "bewildering CSS" problem, and see
how focusing on layout algorithms can help us solve it.
Here we have a basketful of cats:
Code Playground
Format code using Prettier
Reset code
HTMLCSS
[
Result
Refresh results pane
[ ]
Enable 'tab' key
Hmm... Why is there a bit of extra space underneath the image?
If you inspect it with your developer tools, you'll notice a
discrepancy of a few pixels:
The image is 250px tall, but the container is 258.5px tall!
If you're familiar with the box model, you know that elements can be
spaced using padding, border, and margin. You might think that
there's some margin on the image, or some padding on the container?
In this case, none of these properties are responsible. And that's
why, for years, I've privately referred to this as "inline magic
space". It isn't caused by the usual culprits.
To understand what's going on here, we have to dig a bit deeper into
Flow layout.
Link to this heading
Flow layout
As mentioned, Flow layout is designed for documents, similar to
word-processing software.
Documents have the following structure:
* Individual characters are assembled into words and sentences.
These elements sit inline, side-by-side, and line-wrap when there
isn't enough horizontal space.
* Paragraphs are considered blocks, like headings or images. Blocks
will be stacked vertically, one on top of the other, from the top
down.
Flow layout is based on this structure. Individual elements can be
arranged as inline elements (side-by-side, like words in a
paragraph), or as block elements (chunky bricks stacked from the top
down):
The same page as earlier, but with animated annotations. Labels show
the block direction (vertical) and the inline direction (horizontal).
[block-labe][pink-box-1][pink-box-2][pink-box-3][pink-box-4]
[inline-lab][purple-cir][purple-cir][purple-cir][purple-cir]
[purple-cir][purple-cir][purple-cir][purple-cir]
Directions vary by language
In this example, I'm assuming that we're working with a horizontal,
left-to-right language like English. This is not universally the
case!
Some languages, like Arabic and Hebrew, are written horizontally
right-to-left. Other Han-based languages like Chinese, Japanese, and
Korean are traditionally written vertically, from top to bottom.
It's becoming popular to write CSS in a way that accommodates these
differences. For example, we could write margin-inline-start to
target left-side margin in English, and right-side margin in Arabic.
Most HTML elements come with sensible defaults. and
are
considered block-level elements, while and are
considered inline.
Inline elements are meant to be used in the middle of paragraphs, not
as part of the layout. For example, maybe we want to add a little
icon to the middle of a sentence.
In order to make sure that inline elements don't negatively affect
the legibility of the surrounding text, a bit of extra vertical space
is added.
So, bringing this back to our mystery: why does our image have a few
extra pixels of space? Because images are inline elements by default!
The Flow layout algorithm is treating this image as if it was a
character in a paragraph, and adding a bit of space below to ensure
it isn't uncomfortably close to the characters on the (theoretical)
next line of text.
By default, inline elements are "baseline" aligned. This means that
the bottom of the image will align with the invisible horizontal line
that text sits on. That's why there's some space below the image --
that space is for the descenders, like the letters j and p.
So it's not margin, or padding, or border... it's the bit of intrinsic
space that Flow layout applies to inline elements.
Link to this heading
Solving the problem
There are a number of ways to solve this problem. Perhaps the
simplest is to treat this image as a block, within Flow layout:
Code Playground
Format code using Prettier
Reset code
HTMLCSS
[
Result
Refresh results pane
[ ]
Enable 'tab' key
Inline magic space has bitten me many times throughout my career, and
so I've incorporated this exact fix into my custom CSS Reset.
Alternatively, because this behavior is unique to Flow layout, we
could flip to a different layout algorithm:
Code Playground
Format code using Prettier
Reset code
HTMLCSS
[
Result
Refresh results pane
[ ]
Enable 'tab' key
Finally, we could also solve this by shrinking the additional space
to 0, using line-height:
Code Playground
Format code using Prettier
Reset code
HTMLCSS
[
Result
Refresh results pane
[ ]
Enable 'tab' key
This solution removes all additional line spacing by setting it to 0.
This would make multi-line text totally unreadable, but since this
container contains no text, it's not an issue.
I would recommend using one of the earlier two solutions. This one is
presented purely because it's interesting (and because it proves that
the issue is due to line spacing!).
Line height and accessibility
While we're talking about line-height: did you know that "unstyled"
HTML is actually considered inaccessible, because the lines are too
close together? It's hard for folks with dyslexia to parse text when
the lines aren't generously spaced.
Most browsers ship with a default line-height of 1.1 to 1.2, but
according to the WCAG guidelines, we should be setting it to at least
1.5 for body text.
Check out my custom CSS Reset to see how I tackle this problem!
Link to this heading
Building an intuition
So, here's the point: If you were focusing exclusively on studying
what specific CSS properties do, you'd never understand where this
mysterious space is coming from. It isn't explained in the MDN pages
for display or line-height.
As we've learned in this post, "inline magic space" isn't really
magic at all. It's caused by a rule within the Flow layout algorithm
that inline elements should be affected by line-height. But it seemed
magical to me, for many years, because I had this big hole in my
mental model.
There are a lot of layout algorithms in CSS, and they all have their
own quirks and hidden mechanisms. When we focus on CSS properties,
we're only seeing the tip of the iceberg. We never learn about really
important concepts like stacking contexts or containing blocks or
cascade origins!
Unfortunately, a lot of CSS instruction online is equally shallow.
It's common for a blog post or tweet to share a handy CSS snippet,
without explaining why it works, or how the layout algorithms use it.
*
CSS is a tricky language to debug; we don't have error messages, or
debugger, or console.log. Our intuition is the best tool we have. And
when we start using CSS snippets without truly understanding them,
it's only a matter of time until some hidden aspect of the layout
algorithm throws a wrench into our gears, stopping us in our tracks.
A few years ago, I decided to start building my intuition for CSS.
Whenever I was derailed by some unexpected behaviour, I'd settle into
the problem as though it were a warm bath. I'd dig deeper into the
MDN documentation and the CSSWG specification, and tinker with the
code until I felt I had really figured out what was going on.
This was an absolutely worthwhile investment, but my goodness, it
took forever.
I want to speed this process up for other developers. I recently
released a comprehensive online course called CSS for JavaScript
Developers.
Banner with text "CSS for JavaScript Developers"
In this course, we explore how CSS works under the hood. It's
laser-focused on providing a robust mental model you can use, one
that helps build your CSS intuition one puzzle piece at a time. I
can't promise you'll never hit a CSS challenge again, but I can help
you build the toolkit you need to overcome them.
More than 9500 developers have taken the course so far, from
organizations like Facebook, Google, Microsoft, Netflix, and many
many more. The response has been overwhelmingly positive.
You can learn more on the course homepage:
https://css-for-js.dev.
A front-end web development newsletter that sparks joy
My goal with this blog is to create helpful content for front-end web
devs, and my newsletter is no different! I'll let you know when I
publish new content, and I'll even share exclusive newsletter-only
content now and then.
No spam, unsubscribe at any time.
First Name
[ ]
Email
[ ]
Subscribe
Last Updated:
March 28th, 2022
JoshWComeau
Thanks for reading!
(c) 2020-present Joshua Comeau. All Rights Reserved.
Tutorials
ReactAnimationCSSCareerGatsbyNext.jsPerformanceJavaScript
Links
TwitterRSSContact
(c) 2020-present Joshua Comeau. All Rights Reserved.