CSS as a System of Layout Engines

CSS is often taught as a checklist of properties and their values. Memorize z-index, justify-content, and a hundred others, and the language should make sense. In practice, that approach breaks down. The same CSS can produce different results in different contexts, which makes the language feel unreliable.

The missing piece is that CSS is not a flat collection of properties. It is a set of layout algorithms, each with its own rules. Properties are inputs to those algorithms, not standalone behaviors. A property only does something if the layout algorithm in use chooses to implement it.

The Core Layout Modes

The browser renders every element using a primary layout algorithm. The main ones are:

  • Flow
  • Flexbox
  • Grid
  • Positioned (e.g. position: absolute)
  • Table

These are technically called layout modes, but the term "layout algorithm" is more descriptive of what actually happens. You opt into a different algorithm with declarations like position: absolute or display: flex.

Flow layout is the default for most elements. It is modeled on document typesetting, similar to word processors. That heritage matters because Flow layout simply does not implement some properties that other algorithms support.

A Concrete Example: z-index

Consider this CSS:

.box {
  z-index: 10;
}

That .box element renders with Flow layout. Setting z-index on it will have no effect because the Flow algorithm does not implement the property. A common explanation is that z-index requires a position value like relative or absolute. That is a subtle misreading. The issue is not a dependency between properties; it is that Flow layout never consults z-index as part of its calculations.

Switch that element to a Flex container, however, and the picture changes:

<style>  .row {    display: flex;    gap: 16px;  }  .raised.item {    z-index: 2;    background: hotpink;  }</style><ul class="row">  <li class="item"></li>  <li class="raised item"></li>  <li class="item"></li></ul>

Here we have three siblings in a Flexbox layout. The middle sibling sets z-index — and it works, with no position: relative anywhere. Removing it drops the middle element behind its siblings.

The Flexbox algorithm implements z-index for stacking order. The language authors wired it up when they designed that algorithm. The property itself is neutral; each layout algorithm decides whether and how to use it.

Some properties behave identically everywhere. color: red renders red text no matter the layout mode. Many others, like z-index, have no default behavior at all and only exist within particular algorithms.

Property Semantics Vary by Algorithm

The width property is arguably the clearest demonstration that layout algorithms interpret inputs differently. Two instances of the same element with the same stylesheet can render very differently:

<style>  .flex-wrapper {    display: flex;  }  .item {    width: 2000px;  }</style><div class="item"></div><div class="flex-wrapper">  <div class="item"></div></div>

Here, .item has a single property: width: 2000px.

In Flow layout, width is honored as a hard constraint. That element will consume 2000px of horizontal space. The Flexbox algorithm treats width much differently. Flexbox computes a hypothetical size — the width the element would have in an unconstrained world. When space is limited, a flex item shrinks below that hypothetical size.

The right mental model is not that width has special caveats in Flexbox. The Flexbox algorithm simply implements the property in a different way than Flow layout does. Properties are inputs, like arguments passed to a function. The layout algorithm owns the logic that decides what to do with them.

Identifying the Active Layout Algorithm

There is no layout-mode property in CSS. Determining which algorithm an element uses can require checking both the element's own declarations and its parent's. In some cases a property directly opts an element into a layout mode:

.help-widget {
  /* Uses Positioned layout, because of this declaration: */
  position: fixed;
  right: 0;
  bottom: 0;
}

.floated {
  /* Uses Float layout, because of this declaration: */
  float: left;
  margin-right: 32px;
}

In other cases the parent's styles are the determining factor:

<style>
  .row {
    display: flex;
  }
</style>

<ul class="row">
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
</ul>

Applying display: flex does not switch the .row element itself to Flexbox layout. What it does is create a flex formatting context, meaning the element's direct children will use Flexbox layout instead of Flow. The parent does become a block-level box, so the declaration has some effect on how the parent itself participates in the surrounding layout, but the algorithm running on the parent's own content is unchanged.

Layout Variants

Several layout algorithms are broken into variants that share behavior. Positioned layout encompasses four positioning schemes:

  • Relative
  • Absolute
  • Fixed
  • Sticky

These variants behave like related mini-algorithms; for instance, all of them support z-index. Flow layout likewise splits into block and inline flows, which will matter when looking at the algorithm's details.

When Layout Modes Conflict

What happens when multiple algorithms claim the same element?

<style>
  .row {
    display: flex;
  }

  .primary.item {
    position: absolute;
  }
</style>

<ul class="row">
  <li class="item"></li>
  <li class="primary item"></li>
  <li class="item"></li>
</ul>

Here, three list items sit inside a flex container, so they should render with Flexbox layout. The middle child, however, declares position: absolute, pulling itself into Positioned layout. Layout modes have a priority order, similar to CSS specificity. Positioned layout generally wins over other modes.

The consequence is that the Flexbox algorithm considers only two children when calculating layout. The absolutely positioned middle child is invisible to the Flexbox calculations that run on its siblings. In practice these conflicts are rare and usually intentional, but when an element behaves unexpectedly, the first debugging step is to confirm which layout algorithm actually owns that element. The answer is often surprising.

The Case of the Mystery Space

Every CSS developer eventually hits a layout problem that seems to defy the box model. Here's a classic: an image inside a container, with a few extra pixels of space underneath it that no margin, padding, or border can explain.

<div class="photo-wrapper">  <img    class="cat-photo"    alt="A basketful of cats"    src="/images/cats.jpg"  /></div>

Result

Inspect the element, and you'll see the image is 250px tall while its parent container is 258.5px. The discrepancy isn't caused by any of the usual spacing properties — it's the result of how the Flow layout algorithm treats inline elements.

A Document's Blueprint

Flow layout is modeled on word-processing documents. Text characters and small elements like icons sit inline, flowing side-by-side and wrapping when horizontal space runs out. Larger structures — paragraphs, headings, images — are block elements that stack vertically from the top down.

The same page as earlier, but with animated annotations. Labels show the block direction (vertical) and the inline direction (horizontal).

By default, <p> and <h1> are block-level, while <span> and <strong> are inline. Inline elements are intended to sit within a line of text — a small icon inside a sentence, for example. To keep such elements from disrupting text legibility, Flow layout reserves extra vertical space around them.

Images are inline elements by default. This particular image is being treated as if it were a character on a line of text. Inline content aligns to the text baseline, the invisible line characters sit on. The space below is reserved for descenders — the tails on letters like j and p. It's not margin or padding; it's intrinsic line spacing from the layout algorithm.

Three Ways to Fix It

The most straightforward solution is to force the image out of the inline context by making it a block-level element:

<style>  .cat-photo {    display: block;  }</style><div class="photo-wrapper">  <img    class="cat-photo"    alt="A basketful of cats"    src="/images/cats.jpg"  /></div>

Result

This is such a common annoyance that many developers include this rule in a base stylesheet reset. Alternatively, you can escape Flow layout altogether by switching the container to a different algorithm:

<style>  /*    We flip its *parent* to Flex,    so that the child will use    Flexbox instead of Flow:  */  .photo-wrapper {    display: flex;  }</style><div class="photo-wrapper">  <img    class="cat-photo"    alt="A basketful of cats"    src="/images/cats.jpg"  /></div>

Result

A third option is to shrink the line spacing itself to zero with line-height:

<style>  .photo-wrapper {    line-height: 0;  }</style><div class="photo-wrapper">  <img    class="cat-photo"    alt="A basketful of cats"    src="/images/cats.jpg"  /></div>

Result

This works only because the container holds no text — setting line-height to 0 would make multi-line paragraphs unreadable. The first two fixes are better for real code; this one is chiefly a useful demonstration of where the space comes from.

Why This Matters

Studying CSS properties in isolation wouldn't help you diagnose this problem. Nothing about display or line-height in the documentation explains phantom spacing under images. The cause lives inside the Flow layout algorithm — a rule that inline content is affected by line-height.

CSS has multiple layout algorithms, and each has its own hidden mechanisms: stacking contexts, containing blocks, cascade origins. Property-focused tutorials show only the surface layer. Common advice to "add display: block to images" works, but rarely explains why. Without that underlying understanding, every unexpected behavior becomes a guessing game.

There are no console.log statements or error messages in CSS. Debugging relies on intuition, and snippets copied without true comprehension will eventually fail. The better path is to dig into the documentation and tinker with code until the behavior makes sense. It's a slower process, but it builds a mental model that holds up when layouts do something unexpected.