Skip to content

A fluid grid to layout page builder blocks

The websites I develop usually contain a page builder that I can extend with custom blocks on a per site basis. All these blocks will vertically be spaced using Stacks. Horizontally each block was traditionally wrapped with a class called fluid-container. This custom class had a width of 100% and a max-width tied to the Tailwind screens sizes. It’s margin was set to 0 auto to center the container and lastly it had some padding on the sides to keep it from touching the device edge.

The problem

Within the fluid container I would typically use a grid that on larger screens existed out of 12 columns. Throughout the website I would use the same gap sizes to get visual vertical rhythm. All good thus far. Things got tricky when I wanted to start an image on column 7 but let it break out of the container and extend to the browser edge. The same would go for, let’s say a visual in a hero section, that would start at the browser edge and line up on the right side with column 6. For these sorts of layouts I had to resort to hacky CSS solutions that never felt great.

The solution

Marco Rieser was busy on a solution based upon this article by Ryan Mulligan and asked me if I would be interested in shipping this solution with my Starter Kit Peak. After playing around I got very excited and set out to simplify the solution and to get it working with Tailwind CSS, within the context of Peak.

The CSS responsible for the grid is tucked a way in a JS based config file, but compiled to CSS it would like something like this.

1.fluid-grid {
2 --col-gap: clamp(1rem, 3vw, 4rem);
3 --content-max-width: theme('screens.xl');
4 
5 --padding-left: clamp(calc(env(safe-area-inset-left, 0rem) + 1rem), 2vw, calc(env(safe-area-inset-left, 0rem) + 2rem));
6 --padding-right: clamp(calc(env(safe-area-inset-right, 0rem) + 1rem), 2vw, calc(env(safe-area-inset-right, 0rem) + 2rem));
7 --col-width: calc((min(calc(100% - var(--padding-left) - var(--padding-right) - 2 * var(--col-gap)), var(--content-max-width)) - 11 * var(--col-gap)) / 12);
8 --side-width: minmax(0, 1fr);
9 
10 display: grid;
11 column-gap: var(--col-gap);
12 grid-template-columns:
13 [full-start] var(--side-width)
14 [content-start col-1] var(--col-width)
15 [col-2] var(--col-width)
16 [col-3] var(--col-width)
17 [col-4] var(--col-width)
18 [col-5] var(--col-width)
19 [col-6] var(--col-width)
20 [col-7] var(--col-width)
21 [col-8] var(--col-width)
22 [col-9] var(--col-width)
23 [col-10] var(--col-width)
24 [col-11] var(--col-width)
25 [col-12] var(--col-width) [content-end]
26 var(--side-width) [full-end];
27}
The CSS responsible for rendering the grid.

There are basically two configurable custom properties: --col-gap and --content-max-width. With the first one you can set the clamp values for how wide the gaps between columns should be and with the second one you define the max width of the grid container. In this case we use screens.xl, which would be 1280px if you didn’t alter your Tailwind screen sizes.

When the screen is smaller than 1280px we want some padding on the sides. For this we also use clamp with a minimum value of the left or right safe-area-inset + 1rem and a max value of the safe-area-inset + 2rem. This makes sure the padding scales beautifully proportionally to the screen width, since we used 2vw as the ideal clamp value.

With the padding all set we can calculate the --col-width. The calculation for this is as follows:

Automatically choose the minimum value (using min()) between:

  1. 100% - --padding-left - --padding-right - 2 * --col-gap and

  2. --content-max-width

Take that value and:

  • subtract 11 * --col-gap and

  • divide by 12.

Lastly, the --side-width is the remaining space between the grid and the browser edge. That will be evenly distributed on both edges using minmax(0, 1fr). The minimum space will be 0, and the maximum space will be 1fr, since the total remaining space accounts for 2fr.

Using min(), clamp() and calc() we can live calculate the column width. And since the --col-gap and both --padding-* values are a clamp value based on the screen width it all scales beautifully when you resize the browser.

The end result is a grid that looks like this.

This widget sends personal data, like your IP to the Tailwind servers out of the EU.
The grid that extends from browser edge to browser edge. Just like used on this website.

The left and right side would be the browser edges and the first columns the 1fr space between. Centered within we see the 12 column grid.

Using the grid

You can use custom utilities or arbitrary values to place items on the grid. E.g: md:col-start-[col-3] md:col-span-8 to let an item start on column 3 and span for 8 columns.

Besides using arbitrary values, the following utilities are present by default to span items on the grid. By default in Peak those are used on Bard sets like: text, image, table, video and pull quote and they can be customised on a per-site basis. On this site, this very text is space-md, and the grid preview you saw earlier is spaced using space-xl, which makes it break out of the text container on larger screens.

1.span-content .span-md, .span-lg, .span-xl {
2 grid-column: content
3}
4.span-full {
5 grid-column: full
6}
7@media screen('md') {
8 .span-md {
9 grid-column: col 3 / span 8
10 }
11 .span-lg {
12 grid-column: col 2 / span 10
13 }
14 .span-xl {
15 grid-column: col 1 / span 12
16 }
17}
18@media screen('lg') {
19 .span-md {
20 grid-column: col 4 / span 6
21 }
22 .span-lg {
23 grid-column: col 3 / span 8
24 }
25 .span-xl {
26 grid-column: col 2 / span 10
27 }
28}
Some default utilities to easily place content onto the grid.

In action those utilities look like this.

This widget sends personal data, like your IP to the Tailwind servers out of the EU.
The various default spacing utilities and utilities with arbitrary values in action.

Note: the individual blocks (or wrappers) are spaced using the new stack utilities. The full width block got a no-space-y class to make sure it’s flush against its direct siblings. Read more on how that works in the article about stacks.

Subgrids

If you want to use nested grids that fall onto the outer fluid-grid, you can use CSS Subgrid to accomplish this. That will look like this:

1<section class="fluid-grid">
2 <div class="span-content grid grid-cols-subgrid">
3 Children will fall on a new 12 column element that aligns with the parent fluid grid using subgrid.
4 </div>
5</section>

In this case all children of span-content will automatically align with the columns of the parent grid.

If you don’t want to use the CSS Subgrid spec yet, Peak exposes the Custom Property col-gap to the Tailwind spacing scale. This means you can use this value on every utility that uses the spacing scale. For example on width, height and margins. But in this case we can use it as a gap value to avoid using Subgrid.

1<section class="fluid-grid">
2 <div class="span-content grid grid-cols-12 gap-fluid-grid-gap">
3 Children will fall on a new 12 column element that aligns with the parent fluid grid.
4 </div>
5</section>

In conclusion

I’ve developed a few websites using the fluid grid now and I love it. It supports every layout I’ve ever needed. However, it’s not there to make your life more difficult, and you don’t have to use the grid rigidly in every spot of your website. You could easily place all the content of your page block within span-content. Within you block, just add a new element and define it a completely new grid for your current use case.

Feel free to reach out to me on Discord or mail if you have any questions. If you want to know how the live code examples on this entry work, read this post.

Photo by Pavel Nekoranec on Unsplash.

Creating an accessible modal view with Alpine in Statamic

  • Statamic
  • Peak
  • Antlers
  • Alpine JS
  • Accessibility