The height Property Isn't Broken — It's Circular

One of the most confusing moments for CSS beginners comes when you set a percentage-based height and watch the element refuse to move. You write height: 50%, but the box stays exactly the same size as its content. Try height: 10000%? Still nothing.

Unlike so much of CSS, this isn't random behavior. It's the predictable result of how the browser calculates width and height in the default flow layout. The two dimensions are resolved in fundamentally opposite directions.

For width, a block-level element looks up the tree to its parent. It expands to occupy all available space based on that parent's size. But for height, the browser looks down the tree. The element shrinkwraps around its children, growing or shrinking to contain them.

When you ask for height: 50%, you create a paradox: the child demands to be based on the parent's size, while the parent demands to be based on the child's size. The browser can't resolve those two competing constraints, so it silently ignores the percentage declaration. It's a circular calculation that never completes.

Escaping the Loop: Knowable Heights

The fix is straightforward: the parent's height must not depend on the child. You need to make the parent's height "knowable" — a term I use to describe any value that can be inferred from the element itself or its ancestors without descending the tree.

One way to achieve this is with a fixed value on the parent:

<style>  main {    height: 300px;    outline: 2px dashed;  }  p {    height: 50%;    background: tomato;  }</style><main>  <p>    Hello world!  </p></main>

Setting height: 300px on the <main> element short-circuits the normal algorithm. The browser no longer needs to consult the children; it locks the box to 300 pixels. Now the child's height: 50% becomes a simple arithmetic problem.

While this works, you should generally avoid pixel-based heights. They don't scale with the user's font-size, and can cause overflow for users with larger default text settings. The rem unit solves that problem while remaining just as explicit:

<style>  main {    /* ✅ Works just like pixels: */    height: 24rem;    outline: 2px dashed;  }  p {    height: 50%;    background: tomato;  }</style><main>  <p>    Hello world!  </p></main>

The "knowable" concept extends recursively. Once a parent has an explicit size, you can freely use percentages inside that branch of the tree — each percentage resolves against its own parent, and all values are calculable from the root anchor.

There's one notable special case: the root <html> element. It doesn't have a parent node to reference. Instead, height: 100% on <html> resolves against the viewport — or, more precisely, against the initial containing block, which is the same size and shape as the viewport.

Because the viewport's dimensions don't depend on your CSS, the root element has a guaranteed knowable height. Until relatively recently, a common reset trick was to chain height: 100% down from <html> to ensure an app's main layout filled the screen:

html, body, #root {
  height: 100%;
}

These days, you can get the same effect more directly with the svh unit (Small Viewport Height), which behaves more consistently on mobile browsers. But understanding percentage chains still matters, since you won't always be sizing relative to the viewport cleanly.

The Trouble with min-height

The complication arises when you swap height for min-height. Suppose you have a fixed layout with content that might overflow; it's tempting to set a parent to something like min-height: 24rem and keep the percentage-based fills inside.

At first that may appear to work. But if you shorten the content enough, the inner element's height: 50% stops working.

This feels counterintuitive. min-height: 24rem is a numeric value just like height: 24rem, so why doesn't it count as a knowable size?

The distinction is critical. Setting min-height does not give the element a fixed size. It establishes a lower bound. The actual rendered height is still dynamic — it's the larger of 24rem or whatever its children require. The parent still depends on the child for its final size, which means the circular dependency that breaks percentage heights remains unsatisfied.

The value on the parent isn't knowable because it isn't fixed; it's flexible. Percentage-based heights never resolve when there's any ambiguity about the parent's final height.

A Better Answer: Grid and Flexbox

The most robust solution comes from changing the layout algorithm entirely. Flexbox and Grid solve this problem because their sizing logic works differently than flow layout.

Consider this approach:

<style>  main {    /* Switch to Grid layout: */    display: grid;    min-height: 24rem;    outline: 2px dashed;  }  .wrapper {    /* No more height required! */    /* height: 100%; */    background: peachpuff;    padding: 1rem;  }</style><main>  <div class="wrapper">    <p>      Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC.    </p>  </div></main>

Setting display: grid on the parent creates a grid formatting context. In a grid, the child does not shrinkwrap around its content. Instead, the default single row and column stretch to fill the entire grid surface. The child grows automatically to occupy its entire grid cell, both horizontally and vertically, so you don't even need to explicitly state height: 100% on the inner element.

Flexbox works similarly, with a small adjustment:

<style>  main {    display: flex;    min-height: 24rem;    outline: 2px dashed;  }  .wrapper {    /* Grow to fill the available space: */    flex: 1;    background: peachpuff;    padding: 1rem;  }</style><main>  <div class="wrapper">    <p>      Hello World!    </p>  </div></main>

In flexbox, you need to set flex: 1 on the child for it to fill the available space along the primary axis. This explicitly instructs the flexible item to grow to consume the extra space.

CSS is effectively a collection of mini-languages, each tailored to a different task. Flow layout is the default and works well for documents. But for building structured UI components, Flexbox or Grid will more readily give you the stretching behavior you want — and are far less surprising when you need filler content to consume the full available area. When you find yourself wrestling with percentage-based height, ask whether you're using the right formatting context for the job, rather than writing increasingly convoluted workarounds.