Equal Height Holy Grail Layout in Modern CSS

Cheap and easy ways to get paths out of your web server logs.

Reposted from Learn JS the Hard Way By Zed A. Shaw

Equal Height Holy Grail Layout in Modern CSS

I constantly forget all of the random steps necessary to make equal height columns so I'm writing this down and sharing it. Most of the information out there on how exactly to create the "holy grail" layout is fairly lacking in explaining why each setting works, or is too old and doesn't use flexbox. A lot of the existing documentation also glosses over everything that's actually needed, and just says "flexbox! yay!" This blog post attempts to explain everything used to make this style of layout, but if you find there could be improvements then please let me know.

TLDR

To create the "holy grail" layout you need to solve three things:

  1. Full height center sandwiched between a header and footer.
  2. The center has two or more columns that are equal height no matter what content they contain.
  3. Avoiding fixed heights on the center columns so content doesn't "explode" out.

With flexbox this becomes easier, but it is still more complex than usually explained. The actual requirements are:

  1. All blocks set to display: flex.
  2. <header> and <footer> set to a var(--height-footer) and var(--height-header) variables to fix the height.
  3. A <main> (grandparent) tag set to flex-direction: column and a height calculation of calc(100vh - var(--height-header) - var(--height-footer)).
  4. A <main><columns> (parent) tag set to flex-direction: row and flex: 1 1 auto.
  5. A <main><columns><left> and <right> (child) set to flex: 1 1 auto with display: block optional if you don't want its contents to flex.
  6. Set the :root{} variables --height-header and --height-footer to whatever you want for them.
  7. Reset <body> to have margin: 0px and padding: 0px so everything is full screen.

This will give you a starter "holy grail" layout that you can then adjust and alter depending on what you need.

What is "The Holy Grail"?

The "Holy Grail" comes from the period right after everyone on the internet decided that <table> tags shall be henceforth banned by Edict of Zeldman. The layout is this:

Seems simple right? You have a header, usually with a navigation element, content with a left and right side (usually of different widths), and then a footer with a ton of links in it. The problem comes when you aren't allowed to use a <table> and have to use only <div> tags. You're then stuck using crazy CSS tricks that barely work, and ultimately fail when you switch to mobile.

Modern CSS lets you use flexbox and grid to create the holy grail layout more easily, and in this article I'll show you how to use flexboxto do this. I'll use grid in another blog post to compare the advantages/disadvantages. This description will also explain why this works so you (and I) can hopefully remember this in the future.

Caveats

Keep in mind this is a starter and not meant to be your entire layout done for you. It simply solves the key problem of getting a header, footer, main content, and two columns on the screen and filling it in a reliable way. You'll still have to do work if you want it to look different, but starting with this will get you farther than trying to figure it all out on your own.

HTML Headers

The simplest HTML headers to get started are:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset='utf-8'>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />

  <title>Holy Grail Starter</title>
  <style>
  /* CSS goes here. When you see me describe CSS you put it here. */
  </style>
</head>
<!-- body here -->
</html>

Your next step is to create the first body, header, and footer tags:

<body>

  <header>
    <h1>Header</h1>
  </header>

  <!-- replace this with the next step -->

  <footer>
    <h1>Footer</h1>
  </footer>
</body>

This needs to be modified with the following CSS:

:root {
  --height-header: 80px;
  --height-footer: 200px;
}

body {
  margin: 0px;
  padding: 0px;
}

footer {
  display: flex;
  background-color: hsl(0, 0%, 80%);
  min-height: var(--height-footer);
}

header {
  display: flex;
  background-color: hsl(0, 0%, 80%);
  height: var(--height-header);
}

In my CSS here I'm setting the <header> and <footer> to have background-color: hsl(0, 0%, 80%); which makes them gray. Change this later when you figure out your design. You should also note the use of :root{} variables --height-header: and --height-footer as they help with the height calculations later.

Main, Columns

Once you have your HTML up and running you can create the <main> and <columns> tags that are sandwiched between the <header> and <footer>:

<main>
  <columns>
    <!-- next step left and right go here -->
  </columns>
</main>

Now comes the "trick" which is to set <main> to use column orientation, and then set <columns> to row orientation. Confused? You should be because column and row are almost entirely arbitrary, but I'll try to explain why this is going to work after you see the CSS:

main {
  display: flex;
  flex-direction: column;
  min-height: calc(100vh - var(--height-header) - var(--height-footer));
}

main > columns {
  display: flex;
  flex-direction: row;
  flex: 1 1 auto;
}

To understand what's going on you need to know how flexbox orients things (which also explains why "row" and "column" is so confusing):

  1. flex-direction determines something called the "main axis" and "cross axis" (or dimension), which is the direction that elements are "stacked" or arranged.
  2. The flex-direction: column means to set the main axis to go vertically, or "along a column" which is up and down. This stacks elements like a stack of books.
  3. That makes flex-direction: row set the main axis along the row, or horizontally. This stacks elements like beads on an abacus.
  4. The easiest way to remember this is to use the fake words "hoROWzontally" and "vertiCOLUMNly". If you want the flex-direction to go horizontally, then say "hoROWzontally" and you'll know to use row. If you want it to go vertically then say "vertiCOLUMNly" and you'll know to use column.
  5. The main axis is the direction that child elements are "stacked", and the direction their size flexes. This means that column stacks vertically (vertiCOLUMNly) like a normal HTML page, and row stacks horizontally (hoROWzontally) like text in this sentence.
  6. The cross axis is the direction that is the inverse of the chosen flex-direction, so if you choose column (vertiCOLUMNly) then the cross axis is row (hoROWzontally).
  7. Flexbox guarantees that all blocks are stretched to fill the cross axis unless told otherwise. Mark this idea down as it's what makes our next step work.
  8. The flex: 1 1 auto property applies a "grow shrink basis" modifier to this child using the parent's flex-direction. I'm constantly tripped up and think that flex: says how I want children to be sized, but it goes on the child and determines how it will grow. You'll find whether a property is for a child or a parent is rarely mentioned in the flexbox specification.
  9. By applying flex: 1 1 auto to main > columns { we're saying, "Expand <columns> to fill the <main> parent's main axis (vertiCOLUMNly)."
  10. Since the main { rule is set to flex-direction: column our <columns> tag will now stretch to the bottom of main, and because it automatically stretches along the cross axis (horizontally) this makes it stretch both ways.

That's a complex breakdown, but the simplest explanation is that you need make the <columns> element to stretch across both the vertical and horizontal dimensions (axis) of <main>. Since flexbox will do this across one dimension by default, you only need to set <columns> to flex: 1 1 auto to make it also stretch across the other dimension.

I did some testing and even though the flex property is part of flexbox it doesn't need to be in a block set to display:flex. I'm not sure if this is standard, but it did work in display:block definitions so it must only require a parent set to display:flex. This makes sense since there's certain points when you don't want to use flexbox, but you still want to size that element.

The min-height Math

In the previous section I had this seemingly complicated and potentially dubious math:

main {
  min-height: calc(100vh - var(--height-header) - var(--height-footer));
}

This uses a fixed height <header> and <footer> to create a center <main> that fills the remaining space. It does this by:

  1. min-height requires that main not shrink below this height.
  2. calc is a function for calculating math.
  3. var references the variables --height-header and --height-footer we set in :root{ above.
  4. 100vh means "100% of the viewport height" so if your browser window is 500px tall then 100vh becomes 500px.
  5. Putting it all together it will set the min-height of main to 100% of the browser window viewport minus the <header> and <footer> height.

Does this mean that if the contents are greater than this height you'll get the classic CSS "explode out of box" problem? Not that I've seen, but then again CSS will surprise you and one day, for some random reason, your content will explode out or weirdly overlap. This setting seems to work as follows:

  1. If the content is less than the screen height - (header + footer) then it will fill the screen and stretch the space inside <columns> to fill that vertical space.
  2. If the screen height shrinks to below the height of the inner columns then it will add scrollbars and not explode out.

If you see this acting differently please let me know and hopefully someone can explain why exactly this works or doesn't work.

Fixed Size Causes Explode Out

When a block has a fixed height you'll see strange "exploding out" of the contents. Normally this is obvious since it happens inside blocks where you've set a min/max height to a specific value, but sometimes it's deceptively hidden. Add this to the main { CSS block:

max-height: 300px;

After you refresh this should look normal, depending on your screen height. It's subtle, but shrink the height of your window and you should see the text of <footer> float above the left column. If you scroll down you'll see that actually the entire <footer> block is sliding under the main content as you shrink it down. Weirdly, this also happens long before you reach the 300px setting, and actually happens when the contents of <left> are reached.

Even though everyone says that "exploding out" of the box only happens when you set the size of that box, it seems that the browser will punish you for setting any size on anything. We set a min-height: on main, so now the browser takes liberty to completely screw up your layout in an unexpected way by not exploding the contents of <left> out. No, it...scrolls <footer> under <main>?

Another way to put this is, according to the "rules", setting a min-height: on <main> should cause the contents of <left> to explode out. It's entirely evil and unexpected to have <footer> slide under <main> instead, but I'm sure some CSS aficionado will have a detailed explanation that still doesn't forgive this behavior.

Left, Right

Inside <columns> you'll have <left> and <right> children that contain your actual content. Here's an abbreviated sample as an example:

<left>
    <!-- put any amount of content here -->
</left>
<right>
    <!-- try less content here to test different heights -->
</right>

If you stop here and refresh the file you'll see that these two columns fill the vertical space, but they don't stretch across the horizontal space. We can break down why this happens like this:

  1. <columns> is set to flex-direction: row so its main axis is hoROWzontal leaving its cross axis as vertical.
  2. Flexbox automatically stretches children along the cross axis (vertical), but by default compacts elements along the main axis (hoROWzontal).
  3. That means, vertically (cross) the <left> and <right> will stretch, and hoROWzontally (main) it will compress because of flex-direction: row.

To fix this we have to apply the same trick from <columns> and add flex: 1 1 auto to the <left> and <right> elements:

The CSS for this part is:

main > columns left,
main > columns right {
  display: block;
  flex: 1 1 auto;
}

Notice here that I'm adding display: block to show that you don't need these elements to be display: flex for the flex: 1 1 auto setting to work. This lets you stop using flexbox but still get the flexbox layout technology. If you were to put another block inside <left> or <right> then you would want to set <left> or <right> to display: flex so you can apply these same techniques again. You'll see this in the later section Two Tone Columns.

The flex: 1 1 auto is a combination of flex-grow, flex-shrink, and flex-basis. These are too complicated for one blog post, so just remember the magic incantation of flex: 1 1 auto when you want something to "fill the space." If you want one element to be smaller than the other, then either change "auto" to be a fixed size, or try changing the "1 1" ratios until it's what you want.

I also add some color to the left and right so I can see it for debugging to make sure they are equal height.

main > columns left {
  background-color: hsl(0, 0%, 100%);
}

main > columns right {
  background-color: hsl(0, 0%, 60%);
}

The Result

When you're done you should have something like this:

Try messing with the width, height, and contents of each block. You should try to break this in different ways and then find solutions. I give a few in the next sections.

Smaller Left/Right

The current layout is doing an equal sized left and right, which isn't usually what people want. I started with this because it's easiest to get working first. If you want the <left> side to be smaller than the <right> side remove this rule:

main > columns left,
main > columns right {
  display: block;
  flex: 1 1 auto;
}

And change these two:

main > columns left {
  display: block;
  flex: 0 1 300px;
  background-color: hsl(0, 0%, 100%);
}

main > columns right {
  display: block;
  flex: 3 1 100%;
  background-color: hsl(0, 0%, 60%);
}

This adds the flex: property to both so you can specify exactly how they should size hoROWzontally (see <columns>). The flex: 0 1 300px property says to not grow <left> and give it a base size of 300px (width). The flex: 3 1 100% on <right> says to make the right side grow 3x and default basis of 100%.

This will mostly work, but it'll depend on what you put in the left side, and you might need to use the classic width: property instead. If everything works it should look like this:

Two Tone Columns

Another common style is to have the <columns> split in half with two "tones" or colors so the screen is divided exactly in half, but to have the contents inside a different width. You can pull this off by first adding your inner content:

<left>
    <info>
        <h1>Left Info</h1>
    </info>
</left>
<right>
    <info>
        <h1>Right Info</h1>
    </info>
</right>

Next we change the main > columns left and main > columns right rules to implement row-reverse on the left, and row on the right:

main > columns left {
  display: flex;
  flex-direction: row-reverse;
}

main > columns right {
  display: flex;
  flex-direction: row;
  background-color: hsl(0, 0%, 60%);
}

Here's how this CSS works:

  1. If you remember that flex-direction: row stacks hoROWzontally then row-reverse simply reverses the order of blocks.
  2. Putting row-reverse on <left> will cause that side's <info> block to push to the right. That then makes the two <info> elements sit next to each other in the center.
  3. Then there's background-color: hsl(0, 0%, 60%) on the <right> tag so it has a background that covers the entire right side of the screen.

Once that's working we can give the two inside <info> elements a width using flex: 0 1 400px:

main > columns left info {
  flex: 0 1 400px;
  border: 1px solid black;
}

main > columns right info {
  flex: 0 1 400px;
  border: 1px solid black;
}

This sets the <info> elements to have a base size of 400px, but they will shrink as needed (up to the content size...hopefully). The flex: 0 1 400px says, 0 growth, 1 shrink, 400px base width.

I also have a border: 1px solid black on these <info> blocks so you can see them, but remove these if you use this. The end result of these changes is this:

Simple Mobile

Supporting mobile with the first layout requires only changing the direction of the <columns>:

@media only screen and (max-width: 700px) {
  main > columns {
    flex-direction: column;
  }
}

This says when the screen width dips below 700px the browser should apply the rules inside { }. In this code I'm simply changing the flex-direction: from row to column which makes the <left> and <right> elements stack vertiCOLUMNly and that fits on a mobile screen.

If you wanted the <right> element to be on top then you would use column-reverse to reverse the new stack. Either way this is how is should end up:

Getting the Code

You can view all of the code in the site blog git where you can view the following files:


More from Learn Code the Hard Way

Exploring the Replacement for C as an Educational Language

My thoughts so far on finding a replacement for C in teaching compiled languages and memory safety.

ResearchPublished Apr 20, 2024

How to Read Programmer Documentation

An excerpt from Learn Python the Hard Way, 5th Edition that explains how I analyze learn from projects with poor or no documentation (which is most of them).

PythonPublished July 29, 2023

The 5 Simple Rules to the Game of Code

An experimental idea to teach the basics of a Turing machine before teaching loops and branching. Feedback welcome.

PythonPublished July 29, 2023

Announcing _Learn Python the Hard Way_'s Next Edition

Announcing the new version of _Learn Python the Hard Way_ which will be entirely focused on Pre-Beginner Data Science and not web development.

AnnouncementPublished May 11, 2023