Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry’s standard dummy
Notice that the height grows and shrinks with the # of lines, but the width always stays maxed-out, even when there isn’t any content!
Now, I recognize that this isn’t terribly surprising or mindblowing. This feels totally normal. But it gets interesting when we think about what this tells us about how these values are calculated.
When calculating an element’s default width, the browser looks up the tree, to the element’s parent. But when calculating an element’s default height, well, that depends on the element’s children. So the browser has to look down the tree instead.
So, when we tell an element to have width: 50%, that’s no big deal. Browsers already use their parent’s size to calculate their width, so it’s an easy thing to say “OK, take up 50% of that available space”.
But when we tell an element to have height: 50%, it’s a different story:
See the problem? They’re trying to derive their size from each other. It’s a circular calculation that never resolves. A mystery wrapped in a riddle. A paradox. As a result, browsers will ignore the height: 50% declaration on the child.
In order for something like height: 50% to work, the parent’s height can’t depend on the child’s height.
We can set this up by giving the parent an explicit height:
Code Playground
Result
Our parent is given height: 300px, which short-circuits the normal calculation. Instead of calculating a dynamic size based on its children, this tag is locked to a fixed value of 300 pixels.
In that case, the child’s height: 50% is resolvable. We can calculate 50% of 300px.
Now, we generally shouldn’t use the px unit for height. Pixels don’t scale with the user’s chosen text size. Folks with poor vision tend to crank up the default font-size, which can cause text to overflow and layouts to break if we use pixels for our container sizes. I have a separate blog post that covers the accessibility implications of pixels(opens in new tab), if you’d like to learn more about this.
Fortunately, the rem unit works just as well for establishing a fixed, knowable size:
Code Playground
Result
Now, here’s where it gets complicated. We can nest percentage-based heights, as long as they’re all calculated from an explicit size:
Code Playground
Result
The top-level tag defines a fixed height of 24rem. A new .wrapper child takes up 50% of that height, which is calculated to be 12rem. So, even though .wrapper uses a percentage-based height, it’s still a “knowable” value.
By “knowable”, I mean that the value can be inferred from the CSS given to this element or its ancestors higher up in the tree. It doesn’t depend on the size of its descendants. “Knowable” isn’t a term of art, it’s my own word.
And because .wrapper’s height is knowable, we can use a percentage-based height for the within. Whenever an element sets an explicit height in pixels or rems, that entire slice of the DOM tree becomes knowable, and we can use percentages anywhere inside.
So here’s an interesting question: what if we use a percentage-based height on the top-level html element?
Well, let’s give it a shot. On this blog, I use iframes for the “RESULT” pane, so we essentially have our own mini browser window:
Code Playground
Result
Look at that! The tag grows to fill the whole viewport.
This works because the root tag is special. Unlike every other node on the page, doesn’t have a parent, since it’s at the very top of the tree. So when we set height: 100%, it isn’t using some parent element’s content box. Instead, it uses the viewport itself.To be more precise, it uses the “initial containing block”, a rectangle the same size and shape as the viewport.
Crucially, this means that the tag has a knowable height, since the dimensions of the viewport don’t depend at all on the children within. There is no CSS I can write that will affect the width or height of the browser window, after all!
So, percentage-based heights work on the top-level DOM node, and we can funnel that value through the whole tree like a hot potato. For many years, this was a core part of my CSS reset, to ensure that my app’s main layout filled the whole viewport:
html, body, #root { height: 100%;}
These days, this trick isn’t necessary anymore; if we want an element to take up 100% of the viewport, we can use the svh unit (Small Viewport Height; similar to vh but without the funky behaviour on mobile browsers). But it’s still worth understanding how percentage-based heights work, since we don’t always want things to be sized relative to the viewport.
The trouble with setting something like height: 24rem is that it can lead to overflows if there’s too much content to fit in that space:
Code Playground
Result
Let’s try to solve this by swapping height with min-height:
Code Playground
Result
At first glance, that looks great… But if we remove some of the content, we discover that our percentage-based height has stopped working:
Code Playground
Result
This is the sort of thing that always threw me off. I’m still giving the parent an explicit knowable size when I set min-height: 24rem, aren’t I?
It feels that way, at least to me, because we’re using a number, 24rem. But when we use min-height instead of height, we aren’t actually giving the element a fixed size.
Remember, the thing we need is for the parent’s height to not depend on the child’s height. That’s how we avoid the circular paradox thing. And we aren’t fulfilling that condition here; the parent will still grow and shrink based on its children. We’ve set a lower bound, but the actual height can be anything from 24rem to infinity, depending on what’s inside.
So far, all of the examples we’ve seen have been using CSS’ default layout mode, Flow layout. It turns out that both Flexbox and Grid can really help us out here!
Check this out:
Code Playground
Result
Try deleting most of the content inside that , and notice that the peach-colored element still fills its container. This is exactly what we want! 😄
When we set display: grid, we create something called a “grid formatting context”. This means that the child within, .wrapper, will use Grid layout instead of Flow layout.
And in Grid layout, elements don’t shrinkwrap around their children. Instead, children will grow to fill their grid cell, both horizontally and vertically. By default, grids will have a single row and a single column, and that row is stretched across the entire grid surface. This means that we don’t have to set height: 100%. The child grows automatically. ✨
We can also use Flexbox, though we do have to instruct the child to fill the available space in the primary axis with flex: 1:
Code Playground
Result
As I shared in my blog post “Understanding Layout Algorithms”(opens in new tab), CSS is kinda like a constellation of mini-languages, each with its own special purpose. By default, most HTML tags use Flow layout, which is essentially the “Microsoft Word” layout algorithm. It’s great for articles and other digital documents, but it’s not so good for building web app layouts.
You’re already using Flexbox and Grid, I presume, but it’s still surprisingly easy to get caught by this percentage-based height issue. When this happens, the solution is to switch to a more-appropriate layout algorithm. 💖
If you found this blog post useful, you might like to know that I have an entire course on CSS!
My course is called CSS for JavaScript Developers(opens in new tab). It‘s like a supercharged version of this blog: there are interactive articles like this one, but also bite-sized videos, challenging exercises, real-world-inspired projects, and even some minigames 😄.
The course has one goal: to help you build a robust and comprehensive mental model for CSS so that you can use it with confidence and build all sorts of complex UIs without frustration or guesswork.
I created the course primarily for React/Angular/Vue devs, since I knew so many folks in this situation who understood JS well but struggled with CSS. Most of the course, however, is focused on vanilla CSS principles, so even if you’re not a React expert, you may still benefit a ton from the course.
For 226 episodes of my Call Kent podcast, I recorded audio, combined it with my response audio, and ran the whole thing through FFmpeg directly on the same Fly.io machine…
Let’s kick off June — and the beginning of summer — with some fresh inspiration! Artists and designers from across the globe once again tickled their creativity to welcome the…
You want to know how old I am? Fine. I’m old enough that my earliest memories of computing involve loading games from a tape into a tape drive. The player…
Next.js 16.2 introduced a stable Adapter API, built in collaboration with OpenNext, Netlify, Cloudflare, AWS Amplify, and Google Cloud. This post covers how that happened, and the commitments we’re making…
If you work in product management, chances are, you’ve heard about or actively use Claude Code. Originally targeted for engineers, Claude Code is quickly becoming a go-to tool for PMs…