HDT Blog

A Deep Dive into CSS Flex Box

Published on

Flexbox is a remarkably powerful layout mode. When we truly understand how it works, we can build dynamic layouts that respond automatically, rearranging themselves as-needed.

For example, check this out:

800
Drag me!

It uses no media/container queries. Instead of setting arbitrary breakpoints, it uses fluid principles to create a layout that flows seamlessly.

Here's the relevant CSS:

.form {
display: flex;
flex-wrap: wrap;
}
.form > input {
flex: 1 1 10ch;
margin: 0.5rem;
}
.form > input[type='email'] {
flex: 3 1 30ch;
}
.input {
border: none;
background: hsl(0 0% 93%);
border-radius: 0.25rem;
padding: 0.75rem;
}

I remember running into demos like this and being completely confusing. I knew the basics of Flexbox, but this seemed like absolute wizardry!

In this blog post, I want to refine your mental model for Flexbox. We'll build an intuition for how the Flexbox algorithm works, by learning about each of these properties. Whether you're a CSS beginner, or you've been using Flexbox for years, I bet you'll learn quite a bit!

Let's do this!

Introduction to Flexbox

CSS is comprised of many different layout algorithms, known officially as “layout modes”. Each layout mode is its own little sub-language within CSS. The default layout mode is Flow layout, but we can opt in to Flexbox by changing the display property on the parent container:

Show primary Axis

Hello
To
The
World
display:

When we flip display to flex, we create a “flex formatting context”. This means that, by default, all children will be positioned according to the Flexbox layout algorithm.

Each layout algorithm is designed to solve a specific problem. The default “Flow” layout is meant to create digital documents; it's essentially the Microsoft Word layout algorithm. Headings and paragraphs stack vertically as blocks, while things like text, links, and images sit inconspicuously within these blocks.

So, what problem does Flexbox solve? Flexbox is all about arranging a group of items in a row or column, and giving us a ridiculous amount of control over the distribution and alignment of those items. As the name suggests, Flexbox is all about flexibility. We can control whether items grow or shrink, how the extra space is distributed, and more.

Flex direction

As mentioned, Flexbox is all about controlling the distribution of elements in a row or column. By default, items will stack side-by-side in a row, but we can flip to a column with the flex-direction property:

Show primary Axis

Hello
To
The
World
flex-direction:

With flex-direction: row, the primary axis runs horizontally, from left to right. When we flip to flex-direction: column, the primary axis runs vertically, from top to bottom.

In Flexbox, everything is based on the primary axis. The algorithm doesn't care about vertical/horizontal, or even rows/columns. All of the rules are structured around this primary axis, and the cross axis that runs perpendicularly.

This is pretty cool. When we learn the rules of Flexbox, we can switch seamlessly from horizontal layouts to vertical ones. All of the rules adapt automatically. This feature is unique to the Flexbox layout mode.

Alignment

We can change how children are distributed along the primary axis using the justify-content property:

Show primary Axis

Hello
To
The
World
flex-direction:
justify-content:

When it comes to the primary axis, we don't generally think in terms of aligning a single child. Instead, it's all about the distribution of the group.

We can bunch all the items up in a particular spot (with flex-start, center, and flex-end), or we can spread them apart (with space-between, space-around, and space-evenly).

For the cross axis, things are a bit different. We use the align-items property:

Show primary Axis

Hello
To
The
World
flex-direction:
align-items:

It's interesting… With align-items, we have some of the same options as justify-content, but there isn't a perfect overlap.

justify-content
align-items

flex-startcenterflex-end

Why don't they share the same options? We'll unravel this mystery shortly, but first, I need to share one more alignment property: align-self .

Unlike justify-content and align-items, align-self is applied to the child element, not the container. It allows us to change the alignment of a specific child along the cross axis:

Show primary Axis

Hello
To
The
World
flex-direction:
align-self:

align-self has all the same values as align-items. In fact, they change the exact same thing. align-items is syntactic sugar, a convenient shorthand that automatically sets the alignment on all the children at once.

There is no justify-self. To understand why not, we need to dig deeper into the Flexbox algorithm.

Content vs. items

So, based on what we've learned so far, Flexbox might seem pretty arbitrary. Why is it justify-content and align-items, and not justify-items, or align-content?

For that matter, why is there an align-self, but not a justify-self??

These questions get at one of the most important and misunderstood things about Flexbox. To help me explain, I'd like to use a metaphor.

In Flexbox, items are distributed along the primary axis. By default, they're nicely lined up, side-by-side. I can draw a straight horizontal line that skewers all of the children, like a kebab*:

The cross axis is different, though. A straight vertical line will only ever intersect one of the children.

It's less like a kebab, and more like a group of cocktail wieners:

There's a significant difference here. With the cocktail wieners, each item can move along its stick without interfering with any of the other items:

By contrast, with our primary axis skewering each sibling, a single item can’t move along its stick without bumping into its siblings! Try dragging the middle piece side to side:

This is the fundamental difference between the primary/cross axis. When we're talking about alignment in the cross axis, each item can do whatever it wants. In the primary axis, though, we can only think about how to distribute the group.

That's why there's no justify-self. What would it mean for that middle piece to set justify-self: flex-start? There's already another piece there!

And so: we have justify-content to control the distribution of the group along the primary axis, and we have align-items to position each item individually along the cross axis. These are the two main properties we use to manage layout with Flexbox.

There's no justify-items for the same reason that there's no justify-self; when it comes to the primary axis, we have to think of the items as a group, as content that can be distributed.

What about align-content? Actually, this does exist within Flexbox! We'll cover it a little later on, when we talk about the flex-wrap property.

##Hypothetical size

Let's talk about one of the most eye-opening realizations I've had about Flexbox.

Suppose I have the following CSS:

.item {
width: 2000px;
}

A reasonable person might look at this and say: “alright, so we'll get an item that is 2000 pixels wide”. But will that always be true?

Let's test it:

This is interesting, isn't it?

Both items have the exact same CSS applied. They each have width: 2000px. And yet, the first item is much wider than the second!

The difference is the layout mode. The first item is being rendered using Flow layout, and in Flow layout, width is a hard constraint. When we set width: 2000px, we'll get a 2000-pixel wide element, even if it has to burst through the side of the viewport like the Kool-Aid guy?.

In Flexbox, however, the width property is implemented differently. It's more of a suggestion than a hard constraint.

The specification has a name for this: the hypothetical size. It's the size an element would be, in a perfect utopian world, with nothing getting in the way.

Alas, things are rarely so simple. In this case, the limiting factor is that the parent doesn't have room for a 2000px-wide child. And so, the child's size is reduced so that it fits.

This is a core part of the Flexbox philosophy. Things are fluid and flexible and can adjust to the constraints of the world.

Growing and shrinking

So, we've seen that the Flexbox algorithm has some built-in flexibility, with hypothetical sizes. But to really see how fluid Flexbox can be, we need to talk about 3 properties: flex-grow, flex-shrink, and flex-basis.

Let's look at each property.

flex-basis

To put it simply: In a Flex row, flex-basis does the same thing as width. In a Flex column, flex-basis does the same thing as height.

As we've learned, everything in Flexbox is pegged to the primary/cross axis. For example, justify-content will distribute the children along the primary axis, and it works exactly the same way whether the primary axis runs horizontally or vertically.

width and height don't follow this rule, though! width will always affect the horizontal size. It doesn't suddenly become height when we flip flex-direction from row to column.

And so, the Flexbox authors created a generic “size” property called flex-basis. It's like width or height, but pegged to the primary axis, like everything else. It allows us to set the hypothetical size of an element in the primary-axis direction, regardless of whether that's horizontal or vertical.

Give it a shot here. Each child has been given flex-basis: 50px, but you can tweak the first child:

Like we saw with width, flex-basis is more of a suggestion than a hard constraint. At a certain point, there just isn't enough space for all of the elements to sit at their assigned size, and so they have to compromise, in order to avoid an overflow.

flex-grow

By default, elements in a Flex context will shrink down to their minimum comfortable size along the primary axis. This often creates extra space.

We can specify how that space should be consumed with the flex-grow property:

The default value for flex-grow is 0, which means that growing is opt-in. If we want a child to gobble up any extra space in the container, we need to explicitly tell it so.

What if multiple children set flex-grow? In this case, the extra space is divvied up between children, proportionally based on their flex-grow value.

flex-shrink

In most of the examples we've seen so far, we've had extra space to work with. But what if our children are too big for their container?

Let's test it. Try shrinking the container to see what happens:

Interesting, right? Both items shrink, but they shrink proportionally. The first child is always 2x the width of the second child.*

As a friendly reminder, flex-basis serves the same purpose as width. We'll use flex-basis because it's conventional, but we'd get the exact same result if we used width!

flex-basis and width set the elements' hypothetical size. The Flexbox algorithm might shrink elements below this desired size, but by default, they'll always scale together, preserving the ratio between both elements.

Gaps

One of the biggest Flexbox quality-of-life improvements in recent years has been the gap property:

gap is a relatively new addition to the Flexbox language, but it's been implemented across all modern browsers(opens in new tab) since early 2021.

Auto margins

There's one other spacing-related trick I want to share. It's been around since the early days of Flexbox, but it's relatively obscure, and it blew my mind when I first discovered it.

The margin property is used to add space around a specific element. In some layout modes, like Flow and Positioned, it can even be used to center an element, with margin: auto.

Auto margins are much more interesting in Flexbox:

Earlier, we saw how the flex-grow property can gobble up any extra space, applying it to a child.

Auto margins will gobble up the extra space, and apply it to the element's margin. It gives us precise control over where to distribute the extra space.

There are lots of other ways we could have solved this problem: we could have grouped the navigation links in their own Flex container, or we could have grown the first list item with flex-grow. But personally, I like the auto-margins solution. We're treating the extra space as a resource, and deciding exactly where it should go.