We’re going to get into building a somewhat unusual layout. It’s going to be multiple rows of differently sized elements, each with a known aspect ratio (like images), which span the full and exact width of the container.
We’re going to do this with flexbox in CSS and have a deep and very mathematical look at how it works.
How Does Flexbox Distribute Space?
It will be first useful to understand flexbox itself and how the different properties affect how the space is used.
.container {
display: flex;
}Code language: CSS (css)
Flex Properties
Certain things go on the flex container itself.
.container {
display: flex;
flex-direction: row; /* Default. Could be "column" */
gap: 1rem; /* size of the space between items */
flex-wrap: wrap; /* Allows items to wrap. Default is "nowrap" */
}Code language: CSS (css)
Some important properties get applied to the items inside the container. These are a bit trickier to understand and use.
.container {
...
> .item {
flex-basis: 100px; /* The size of the item before growing or shrinking happens. */
flex-grow: 1; /* Factor that decides how much of the remaining space the item receives. */
flex-shrink: 1; /* Factor that decides how much of the exceeding space the item loses. */
}
}Code language: CSS (css)
Assumptions and Simplifications
The full flexbox algorithm defined in the specification is more complex than the model we’re about to get into. Layout is complicated! I want to focus specifically on space distribution here, so to keep things focused, were going to ignore a few aspects of flexbox layout for now.
The things we’re ignoring are:
- The
min-widthandmax-width(or height, or the logical property equivalents) on items. - The algorithm is described as a single distribution step, while the specification says it may redistribute when items reach size limits.
- We’re only considering a flex container with
flex-direction: rowand where the main axis is horizontal.
Conceptual Model
The process for distributing space in the row works as follows.
Initial Free Space
The first step is to determine how much space is available or unavailable in the row. The initial free space is the flex container width minus the sum of the flex-basis of all items in the row and minus the gaps between them.
Positive Initial Free Space (Growing)
If the initial free space is positive, items grow to fill the container:
- If the sum of the
flex-growof all items in the row is less than 1, the free space is scaled by this sum. This means less of the total free space will be distributed, and items will not fill the container. - The free space is distributed among items, proportional to their
flex-growvalues.
Negative Free Space (Shrinking)
If the initial free space is negative, items shrink to fit the container:
- If the sum of the
flex-shrinkof all items in the row is less than 1, the free space is scaled by this sum. This means less of the total free space will be distributed, and items will overflow the container. - Each item has a scaled-flex-shrink, calculated as flex-shrink multiplied by flex-basis. This ensures smaller items aren’t reduced to zero before larger ones shrink noticeably.
- The negative free space is distributed among items, proportional to their scaled
flex-shrinkvalues. Items with larger scaled-flex-shrink receive more of the negative space and therefore shrink more.
What About Wrapping?
- Without wrapping, all items remain in a single row.
- With wrapping enabled, each row contains as many items as fit, including gaps. Items that don’t fit move to the next row. The grow or shrink distribution described above is applied independently to each row.
Mathematical Model
Now let’s formalize what we described conceptually, in mathematical terms. We first define the variables:
Initial free space is:
If initial free space is positive, the item width is:
If initial free space is negative, the item width is:
Again, what about Wrapping?
Without wrapping, n is just the number of items. With wrapping enabled, the number of items in a row is the maximum n such that:
And that’s a wrap! Now we can more or less grasp how flexbox actually works.
The Masonry Layout
Now that we know how flexbox works, we can come back to our objective: a layout consisting of items, each with a known aspect ratio, disposed in rows, with items in the same row sharing the same height, and each row spanning the full width of the container.
Layout Constraints
A. Each item having an aspect ratio:
Which can be rewritten as:
B. Each row spanning the full width of the container:
C. Each item in the same row sharing the same height:
Combining the constraints — plug C into A:
Plug the above into B:
H can be taken outside the summation:
Divide by:
Voilà, this last expression tells us that an H always exists such that it respects the layout constraints.
Plugging the Constraints into the Flexbox Model
A. Because each item has an aspect ratio:
B. For each row to span the full width of the container we need:
We can guarantee that by putting:
We use the first two constraints: apply B to the flexbox item final widths and plug that into A.
Now only the last constraint remains, each item in the same row sharing the same height.
The approach here will be to try to make each term constant independently of the item.
That is the first term. We can put equal to so it simplifies with the denominator. Additionally, we can multiply it by a constant α:
Plug the just-defined FBi into the first term:
Because is the width of the item before growing and shrinking happens, and because wi = • hi (from constraint A), we can see that α is the height of the item before growing and shrinking happens. And because it is the same for every item, we can conclude that for our layout to work, all the items must start at the same height.
Rename α to (cross-basis) so:
is the second term. If we think about it or look back at its expression, we can see that it’s already constant for items in a row.
After plugging the just defined into the third term, it becomes:
Depending on if is positive or negative respectively.
For positive , we can put equal to • β, with β being a constant of choice.
Plug the just defined into the third term:
We can see that β simplify away, and only remains 1 divided by the sum of aspect ratio, which is constant.
In constraint B we put ≥ 1, that now translates to choosing a β big enough such that • β ≥ 1 for every item.
For negative , we can push = 1 so it’s constant and respects constraint B ( ≥ 1).
The third term becomes:
Results
That’s it! We found some simple enough solutions. So to keep going here, for both positive and negative :
is a constant of choice, representing the cross-basis, meaning the height of the item before growing or shrinking happens.
The Intuition
All items start at the same height thanks to flex-basis.
When flex-grow is tied to an items aspect ratio, wider items expand more in width than narrower ones. On the flip side, with flex-shrink set to 1, items shrink in proportion to their width, so wider items lose more space than narrow ones.
Even though widths change at different rates, the height of all items changes evenly.
The result is that no matter the aspect ratio of the items, they end up the same height.
Demos
No wrapping:
With wrapping:
With limits:
With images:
