Flexbox Masonry Layout (Explained with Math)

Ibrahim Bendebka Ibrahim Bendebka on

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-width and max-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: row and 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-grow of 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-grow values.

Negative Free Space (Shrinking)

If the initial free space is negative, items shrink to fit the container:

  • If the sum of the flex-shrink of 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-shrink values. 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:

wifinal width of item iw_i \quad \text{final width of item } i
FBiflex-basis of item iFB_i \quad \text{flex-basis of item } i
FGiflex-grow of item iFG_i \quad \text{flex-grow of item } i
FSHiflex-shrink of item iFSH_i \quad \text{flex-shrink of item } i
IFSinitial free spaceIFS \quad \text{initial free space}
nnumber of items in a rown \quad \text{number of items in a row}

Initial free space is:

IFS=ContainerWidthi=1nFBiGapWidth(n1)IFS = ContainerWidth – \sum_{i=1}^{n} FB_i – GapWidth \cdot (n – 1)

If initial free space is positive, the item width is:

wi=FBi+IFSFGik=1nFGkmin(1,k=1nFGk)w_i = FB_i + IFS \cdot \frac{FG_i}{\sum_{k=1}^{n} FG_k} \cdot \min\left(1, \sum_{k=1}^{n} FG_k\right)

If initial free space is negative, the item width is:

wi=FBi+IFSFSHiFBik=1nFSHkFBkmin(1,k=1nFSHk)w_i = FB_i + IFS \cdot \frac{FSH_i \cdot FB_i}{\sum_{k=1}^{n} FSH_k \cdot FB_k} \cdot \min\left(1, \sum_{k=1}^{n} FSH_k\right)

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:

i=1nFBi+GapWidth(n1)ContainerWidth\sum_{i=1}^{n} FB_i + GapWidth \cdot (n – 1) \leq ContainerWidth

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:

ARi=wihiAR_i = \frac{w_i}{h_i}

Which can be rewritten as:

wi=ARihiorhi=wiARiw_i = AR_i \cdot h_i \qquad \text{or} \qquad h_i = \frac{w_i}{AR_i}

B. Each row spanning the full width of the container:

i=1nwi=ContainerWidth\sum_{i=1}^{n} w_i = ContainerWidth

C. Each item in the same row sharing the same height:

hi=Hfor every ih_i = H \quad \text{for every } i

Combining the constraints — plug C into A:

wi=ARiHw_i = AR_i \cdot H

Plug the above into B:

i=1nARiH=ContainerWidth\sum_{i=1}^{n} AR_i \cdot H = ContainerWidth

H can be taken outside the summation:

Hi=1nARi=ContainerWidthH \cdot \sum_{i=1}^{n} AR_i = ContainerWidth

Divide by:

i=1nARi\sum_{i=1}^{n} AR_i
H=ContainerWidthi=1nARiH = \frac{ContainerWidth}{\sum_{i=1}^{n} AR_i}

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:

hi=wiARih_i = \frac{w_i}{AR_i}

B. For each row to span the full width of the container we need:

min(1,j=1nFGj)=1andmin(1,j=1nFSHj)=1\min\left(1, \sum_{j=1}^{n} FG_j\right) = 1 \qquad \text{and} \qquad \min\left(1, \sum_{j=1}^{n} FSH_j\right) = 1

We can guarantee that by putting:

FGi1andFSHi1for every iFG_i \geq 1 \qquad \text{and} \qquad FSH_i \geq 1 \quad \text{for every } i

We use the first two constraints: apply B to the flexbox item final widths and plug that into A.

hi=FBiARi+IFSFGiARik=1nFGkandhi=FBiARi+IFSFSHiFBiARik=1nFSHkFBkh_i = \frac{FB_i}{AR_i} + IFS \cdot \frac{FG_i}{AR_i \cdot \sum_{k=1}^{n} FG_k} \qquad \text{and} \qquad h_i = \frac{FB_i}{AR_i} + IFS \cdot \frac{FSH_i \cdot FB_i}{AR_i \cdot \sum_{k=1}^{n} FSH_k \cdot FB_k}

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.

FBiARi\frac{FB_i}{AR_i}

That is the first term. We can put FBi equal to ARi so it simplifies with the denominator. Additionally, we can multiply it by a constant α:

FBi=ARiαFB_i = AR_i \cdot \alpha

Plug the just-defined FBi into the first term:

ARiαARi=α\frac{AR_i \cdot \alpha}{AR_i} = \alpha

Because FBi is the width of the item before growing and shrinking happens, and because wi = ARi • 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 CB (cross-basis) so:

FBi=ARiCBFB_i = AR_i \cdot CB

IFS 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 FBi into the third term, it becomes:

FGiARik=1nFGk or FSHiARiCBARik=1nFSHkARkCB=FSHik=1nFSHkARk( for CB>0)\frac{F G_i}{A R_i \cdot \sum_{k=1}^n F G_k} \quad \text { or } \quad \frac{F S H_i \cdot A R_i \cdot C B}{A R_i \cdot \sum_{k=1}^n F S H_k \cdot A R_k \cdot C B}=\frac{F S H_i}{\sum_{k=1}^n F S H_k \cdot A R_k} \quad(\text { for } C B>0)

Depending on if IFS is positive or negative respectively.

For positive FBi, we can put FGi equal to ARiβ, with β being a constant of choice.

FGi=ARiβFG_i = AR_i \cdot \beta

Plug the just defined FGi into the third term:

ARiβARik=1nARkβ=1k=1nARk\frac{AR_i \cdot \beta}{AR_i \cdot \sum_{k=1}^{n} AR_k \cdot \beta} = \frac{1}{\sum_{k=1}^{n} AR_k}

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 FGi ≥ 1, that now translates to choosing a β big enough such that ARiβ ≥ 1 for every item.

For negative IFS , we can push FSHi = 1 so it’s constant and respects constraint B (FSHi ≥ 1).

The third term becomes:

1k=1nARk\frac{1}{\sum_{k=1}^{n} AR_k}

Results

That’s it! We found some simple enough solutions. So to keep going here, for both positive and negative IFS:

hi=H=CB+IFSk=1nARkh_i = H = CB + \frac{IFS}{\sum_{k=1}^{n} AR_k}
wi=FBi+IFSARik=1nARkw_i = FB_i + IFS \cdot \frac{AR_i}{\sum_{k=1}^{n} AR_k}
FGi=ARiβsuch thatFGi1 for every iFG_i = AR_i \cdot \beta \quad \text{such that} \quad FG_i \geq 1 \text{ for every } i
FSHi=1FSH_i = 1
FBi=ARiCBFB_i = AR_i \cdot CB \quad

CB 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:

Wanna learn modern CSS layout?

Leave a Reply

Your email address will not be published. Required fields are marked *

$966,000

Frontend Masters donates to open source projects through thanks.dev and Open Collective, as well as donates to non-profits like The Last Mile, Annie Canons, and Vets Who Code.