Modern CSS Layouts: You Might Not Need A Framework For That

Establishing layouts in CSS is something that we, as developers, often delegate to whatever framework we’re most comfortable using. And even though it’s possible to configure a framework to get just what we need out of it, how often have you integrated an entire CSS library simply for its layout features? I’m sure many of us have done it at some point, dating back to the days of 960.gs, Bootstrap, Susy, and Foundation.

Modern CSS features have significantly cut the need to reach for a framework simply for its layout. Yet, I continue to see it happen. Or, I empathize with many of my colleagues who find themselves re-creating the same Grid or Flexbox layout over and over again.

In this article, we will gain greater control over web layouts. Specifically, we will create four CSS classes that you will be able to take and use immediately on just about any project or place where you need a particular layout that can be configured to your needs.

While the concepts we cover are key, the real thing I want you to take away from this is the confidence to use CSS for those things we tend to avoid doing ourselves. Layouts used to be a challenge on the same level of styling form controls. Certain creative layouts may still be difficult to pull off, but the way CSS is designed today solves the burdens of the established layout patterns we’ve been outsourcing and re-creating for many years.

What We’re Making

We’re going to establish four CSS classes, each with a different layout approach. The idea is that if you need, say, a fluid layout based on Flexbox, you have it ready. The same goes for the three other classes we’re making.

And what exactly are these classes? Two of them are Flexbox layouts, and the other two are Grid layouts, each for a specific purpose. We’ll even extend the Grid layouts to leverage CSS Subgrid for when that’s needed.

Within those two groups of Flexbox and Grid layouts are two utility classes: one that auto-fills the available space — we’re calling these “fluid” layouts — and another where we have greater control over the columns and rows — we’re calling these “repeating” layouts.

Finally, we’ll integrate CSS Container Queries so that these layouts respond to their own size for responsive behavior rather than the size of the viewport. Where we’ll start, though, is organizing our work into Cascade Layers, which further allow you to control the level of specificity and prevent style conflicts with your own CSS.

Setup: Cascade Layers & CSS Variables

A technique that I’ve used a few times is to define Cascade Layers at the start of a stylesheet. I like this idea not only because it keeps styles neat and organized but also because we can influence the specificity of the styles in each layer by organizing the layers in a specific order. All of this makes the utility classes we’re making easier to maintain and integrate into your own work without running into specificity battles.

I think the following three layers are enough for this work:

@layer reset, theme, layout;

Notice the order because it really, really matters. The reset layer comes first, making it the least specific layer of the bunch. The layout layer comes in at the end, making it the most specific set of styles, giving them higher priority than the styles in the other two layers. If we add an unlayered style, that one would be added last and thus have the highest specificity.

Related: “Getting Started With Cascade Layers” by Stephanie Eckles.

Let’s briefly cover how we’ll use each layer in our work.

Reset Layer

The reset layer will contain styles for any user agent styles we want to “reset”. You can add your own resets here, or if you already have a reset in your project, you can safely move on without this particular layer. However, do remember that un-layered styles will be read last, so wrap them in this layer if needed.

I’m just going to drop in the popular box-sizing declaration that ensures all elements are sized consistently by the border-box in accordance with the CSS Box Model.

@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  body {
    margin: 0;
  }
}

Theme Layer

This layer provides variables scoped to the :root element. I like the idea of scoping variables this high up the chain because layout containers — like the utility classes we’re creating — are often wrappers around lots of other elements, and a global scope ensures that the variables are available anywhere we need them. That said, it is possible to scope these locally to another element if you need to.

Now, whatever makes for “good” default values for the variables will absolutely depend on the project. I’m going to set these with particular values, but do not assume for a moment that you have to stick with them — this is very much a configurable system that you can adapt to your needs.

Here are the only three variables we need for all four layouts:

@layer theme {
  :root {
    --layout-fluid-min: 35ch;
    --layout-default-repeat: 3;
    --layout-default-gap: 3vmax;
  }
}

In order, these map to the following:

Notice: The variables are prefixed with layout-, which I’m using as an identifier for layout-specific values. This is my personal preference for structuring this work, but please choose a naming convention that fits your mental model — naming things can be hard!

Layout Layer

This layer will hold our utility class rulesets, which is where all the magic happens. For the grid, we will include a fifth class specifically for using CSS Subgrid within a grid container for those possible use cases.

@layer layout {  
  .repeating-grid {}
  .repeating-flex {}
  .fluid-grid {}
  .fluid-flex {}

  .subgrid-rows {}
}

Now that all our layers are organized, variables are set, and rulesets are defined, we can begin working on the layouts themselves. We will start with the “repeating” layouts, one based on CSS Grid and the other using Flexbox.

Repeating Grid And Flex Layouts

I think it’s a good idea to start with the “simplest” layout and scale up the complexity from there. So, we’ll tackle the “Repeating Grid” layout first as an introduction to the overarching technique we will be using for the other layouts.

Repeating Grid

If we head into the @layout layer, that’s where we’ll find the .repeating-grid ruleset, where we’ll write the styles for this specific layout. Essentially, we are setting this up as a grid container and applying the variables we created to it to establish layout columns and spacing between them.

.repeating-grid {
  display: grid;
  grid-template-columns: repeat(var(--layout-default-repeat), 1fr);
  gap: var(--layout-default-gap);
}

It’s not too complicated so far, right? We now have a grid container with three equally sized columns that take up one fraction (1fr) of the available space with a gap between them.

This is all fine and dandy, but we do want to take this a step further and turn this into a system where you can configure the number of columns and the size of the gap. I’m going to introduce two new variables scoped to this grid:

  • --_grid-repeat: The number of grid columns.
  • --_repeating-grid-gap: The amount of space between grid items.

Did you notice that I’ve prefixed these variables with an underscore? This was actually a JavaScript convention to specify variables that are “private” — or locally-scoped — before we had const and let to help with that. Feel free to rename these however you see fit, but I wanted to note that up-front in case you’re wondering why the underscore is there.

.repeating-grid {
  --_grid-repeat: var(--grid-repeat, var(--layout-default-repeat));
  --_repeating-grid-gap: var(--grid-gap, var(--layout-default-gap));

  display: grid;
  grid-template-columns: repeat(var(--layout-default-repeat), 1fr);
  gap: var(--layout-default-gap);
}

Notice: These variables are set to the variables in the @theme layer. I like the idea of assigning a global variable to a locally-scoped variable. This way, we get to leverage the default values we set in @theme but can easily override them without interfering anywhere else the global variables are used.

Now let’s put those variables to use on the style rules from before in the same .repeating-grid ruleset:

.repeating-grid {
  --_grid-repeat: var(--grid-repeat, var(--layout-default-repeat));
  --_repeating-grid-gap: var(--grid-gap, var(--layout-default-gap));

  display: grid;
  grid-template-columns: repeat(var(--_grid-repeat), 1fr);
  gap: var(--_repeating-grid-gap);
}

What happens from here when we apply the .repeating-grid to an element in HTML? Let’s imagine that we are working with the following simplified markup:

<section class="repeating-grid">
  <div></div>
  <div></div>
  <div></div>
</section>

If we were to apply a background-color and height to those divs, we would get a nice set of boxes that are placed into three equally-sized columns, where any divs that do not fit on the first row automatically wrap to the next row.

Time to put the process we established with the Repeating Grid layout to use in this Repeating Flex layout. This time, we jump straight to defining the private variables on the .repeating-flex ruleset in the @layout layer since we already know what we’re doing.

.repeating-flex {
  --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));
  --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));
}

Again, we have two locally-scoped variables used to override the default values assigned to the globally-scoped variables. Now, we apply them to the style declarations.

.repeating-flex {
  --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));
  --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));

  display: flex;
  flex-wrap: wrap;
  gap: var(--_repeating-flex-gap);
}

We’re only using one of the variables to set the gap size between flex items at the moment, but that will change in a bit. For now, the important thing to note is that we are using the flex-wrap property to tell Flexbox that it’s OK to let additional items in the layout wrap into multiple rows rather than trying to pack everything in a single row.

But once we do that, we also have to configure how the flex items shrink or expand based on whatever amount of available space is remaining. Let’s nest those styles inside the parent ruleset:

.repeating-flex {
  --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));
  --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));

  display: flex;
  flex-wrap: wrap;
  gap: var(--_repeating-flex-gap);

  > * {
    flex: 1 1 calc((100% / var(--_flex-repeat)) - var(--_gap-repeater-calc));
  }
}

If you’re wondering why I’m using the universal selector (*), it’s because we can’t assume that the layout items will always be divs. Perhaps they are <article> elements, <section>s, or something else entirely. The child combinator (>) ensures that we’re only selecting elements that are direct children of the utility class to prevent leakage into other ancestor styles.

The flex shorthand property is one of those that’s been around for many years now but still seems to mystify many of us. Before we unpack it, did you also notice that we have a new locally-scoped --_gap-repeater-calc variable that needs to be defined? Let’s do this:

.repeating-flex {
  --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));
  --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));

  /* New variables */
  --_gap-count: calc(var(--_flex-repeat) - 1);
  --_gap-repeater-calc: calc(
    var(--_repeating-flex-gap) / var(--_flex-repeat) * var(--_gap-count)
  );

  display: flex;
  flex-wrap: wrap;
  gap: var(--_repeating-flex-gap);

  > * {
    flex: 1 1 calc((100% / var(--_flex-repeat)) - var(--_gap-repeater-calc));
  }
}

Whoa, we actually created a second variable that --_gap-repeater-calc can use to properly calculate the third flex value, which corresponds to the flex-basis property, i.e., the “ideal” size we want the flex items to be.

If we take out the variable abstractions from our code above, then this is what we’re looking at:

.repeating-flex {
  display: flex;
  flex-wrap: wrap;
  gap: 3vmax

  > * {
    flex: 1 1 calc((100% / 3) - calc(3vmax / 3 * 2));
  }
}

Hopefully, this will help you see what sort of math the browser has to do to size the flexible items in the layout. Of course, those values change if the variables’ values change. But, in short, elements that are direct children of the .repeating-flex utility class are allowed to grow (flex-grow: 1) and shrink (flex-shrink: 1) based on the amount of available space while we inform the browser that the initial size (i.e., flex-basis) of each flex item is equal to some calc()-ulated value.

Because we had to introduce a couple of new variables to get here, I’d like to at least explain what they do:

  • --_gap-count: This stores the number of gaps between layout items by subtracting 1 from --_flex-repeat. There’s one less gap in the number of items because there’s no gap before the first item or after the last item.
  • --_gap-repeater-calc: This calculates the total gap size based on the individual item’s gap size and the total number of gaps between items.

From there, we calculate the total gap size more efficiently with the following formula:

calc(var(--_repeating-flex-gap) / var(--_flex-repeat) * var(--_gap-count))

Let’s break that down further because it’s an inception of variables referencing other variables. In this example, we already provided our repeat-counting private variable, which falls back to the default repeater by setting the --layout-default-repeat variable.

This sets a gap, but we’re not done yet because, with flexible containers, we need to define the flex behavior of the container’s direct children so that they grow (flex-grow: 1), shrink (flex-shrink: 1), and with a flex-basis value that is calculated by multiplying the repeater by the total number of gaps between items.

Next, we divide the individual gap size (--_repeating-flex-gap) by the number of repetitions (--_flex-repeat)) to equally distribute the gap size between each item in the layout. Then, we multiply that gap size value by one minus the total number of gaps with the --_gap-count variable.

And that concludes our repeating grids! Pretty fun, or at least interesting, right? I like a bit of math.

Before we move to the final two layout utility classes we’re making, you might be wondering why we want so many abstractions of the same variable, as we start with one globally-scoped variable referenced by a locally-scoped variable which, in turn, can be referenced and overridden again by yet another variable that is locally scoped to another ruleset. We could simply work with the global variable the whole time, but I’ve taken us through the extra steps of abstraction.

I like it this way because of the following:

  1. I can peek at the HTML and instantly see which layout approach is in use: .repeating-grid or .repeating-flex.
  2. It maintains a certain separation of concerns that keeps styles in order without running into specificity conflicts.

See how clear and understandable the markup is:

<section class="repeating-flex footer-usps">
  <div></div>
  <div></div>
  <div></div>
</section>

The corresponding CSS is likely to be a slim ruleset for the semantic .footer-usps class that simply updates variable values:

.footer-usps {
  --flex-repeat: 3;
  --flex-gap: 2rem;
}

This gives me all of the context I need: the type of layout, what it is used for, and where to find the variables. I think that’s handy, but you certainly could get by without the added abstractions if you’re looking to streamline things a bit.

Fluid Grid And Flex Layouts

All the repeating we’ve done until now is fun, and we can manipulate the number of repeats with container queries and media queries. But rather than repeating columns manually, let’s make the browser do the work for us with fluid layouts that automatically fill whatever empty space is available in the layout container. We may sacrifice a small amount of control with these two utilities, but we get to leverage the browser’s ability to “intelligently” place layout items with a few CSS hints.

Fluid Grid

Once again, we’re starting with the variables and working our way to the calculations and style rules. Specifically, we’re defining a variable called --_fluid-grid-min that manages a column’s minimum width.

Let’s take a rather trivial example and say we want a grid column that’s at least 400px wide with a 20px gap. In this situation, we’re essentially working with a two-column grid when the container is greater than 820px wide. If the container is narrower than 820px, the column stretches out to the container’s full width.

If we want to go for a three-column grid instead, the container’s width should be about 1240px wide. It’s all about controlling the minimum sizing values in the gap.

.fluid-grid {
  --_fluid-grid-min: var(--fluid-grid-min, var(--layout-fluid-min));
  --_fluid-grid-gap: var(--grid-gap, var(--layout-default-gap));
}

That establishes the variables we need to calculate and set styles on the .fluid-grid layout. This is the full code we are unpacking:

 .fluid-grid {
  --_fluid-grid-min: var(--fluid-grid-min, var(--layout-fluid-min));
  --_fluid-grid-gap: var(--grid-gap, var(--layout-default-gap));

  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(min(var(--_fluid-grid-min), 100%), 1fr)
  );
  gap: var(--_fluid-grid-gap);
}

The display is set to grid, and the gap between items is based on the --fluid-grid-gap variable. The magic is taking place in the grid-template-columns declaration.

This grid uses the repeat() function just as the .repeating-grid utility does. By declaring auto-fit in the function, the browser automatically packs in as many columns as it possibly can in the amount of available space in the layout container. Any columns that can’t fit on a line simply wrap to the next line and occupy the full space that is available there.

Then there’s the minmax() function for setting the minimum and maximum width of the columns. What’s special here is that we’re nesting yet another function, min(), within minmax() (which, remember, is nested in the repeat() function). This a bit of extra logic that sets the minimum width value of each column somewhere in a range between --_fluid-grid-min and 100%, where 100% is a fallback for when --_fluid-grid-min is undefined or is less than 100%. In other words, each column is at least the full 100% width of the grid container.

The “max” half of minmax() is set to 1fr to ensure that each column grows proportionally and maintains equally sized columns.

See the Pen Fluid grid [forked] by utilitybend.

That’s it for the Fluid Grid layout! That said, please do take note that this is a strong grid, particularly when it is combined with modern relative units, e.g. ch, as it produces a grid that only scales from one column to multiple columns based on the size of the content.

Fluid Flex

We pretty much get to re-use all of the code we wrote for the Repeating Flex layout for the Fluid Flex layout, but only we’re setting the flex-basis of each column by its minimum size rather than the number of columns.

.fluid-flex {
  --_fluid-flex-min: var(--fluid-flex-min, var(--layout-fluid-min));
  --_fluid-flex-gap: var(--flex-gap, var(--layout-default-gap));

  display: flex;
  flex-wrap: wrap;
  gap: var(--_fluid-flex-gap);

  > * {
    flex: 1 1 var(--_fluid-flex-min);
  }
}

That completes the fourth and final layout utility — but there’s one bonus class we can create to use together with the Repeating Grid and Fluid Grid utilities for even more control over each layout.

Optional: Subgrid Utility

Subgrid is handy because it turns any grid item into a grid container of its own that shares the parent container’s track sizing to keep the two containers aligned without having to redefine tracks by hand. It’s got full browser support and makes our layout system just that much more robust. That’s why we can set it up as a utility to use with the Repeating Grid and Fluid Grid layouts if we need any of the layout items to be grid containers for laying out any child elements they contain.

Here we go:

.subgrid-rows {
  > * {
    display: grid;
    gap: var(--subgrid-gap, 0);
    grid-row: auto / span var(--subgrid-rows, 4);
    grid-template-rows: subgrid;
  }
}

We have two new variables, of course:

  • --subgrid-gap: The vertical gap between grid items.
  • --subgrid-rows The number of grid rows defaulted to 4.

We have a bit of a challenge: How do we control the subgrid items in the rows? I see two possible methods.

Method 1: Inline Styles

We already have a variable that can technically be used directly in the HTML as an inline style:

<section class="fluid-grid subgrid-rows" style="--subgrid-rows: 4;">
  <!-- items -->
</section>

This works like a charm since the variable informs the subgrid how much it can grow.

Method 2: Using The :has() Pseudo-Class

This approach leads to verbose CSS, but sacrificing brevity allows us to automate the layout so it handles practically anything we throw at it without having to update an inline style in the markup.

Check this out:

.subgrid-rows {
  &:has(> :nth-child(1):last-child) { --subgrid-rows: 1; }
  &:has(> :nth-child(2):last-child) { --subgrid-rows: 2; }
  &:has(> :nth-child(3):last-child) { --subgrid-rows: 3; }
  &:has(> :nth-child(4):last-child) { --subgrid-rows: 4; }
  &:has(> :nth-child(5):last-child) { --subgrid-rows: 5; }
  /* etc. */

  > * {
    display: grid;
    gap: var(--subgrid-gap, 0);
    grid-row: auto / span var(--subgrid-rows, 5);
    grid-template-rows: subgrid;
  }
}

The :has() selector checks if a subgrid row is the last child item in the container when that item is either the first, second, third, fourth, fifth, and so on item. For example, the second declaration:

&:has(> :nth-child(2):last-child) { --subgrid-rows: 2; }

…is pretty much saying, “If this is the second subgrid item and it happens to be the last item in the container, then set the number of rows to 2.”

Whether this is too heavy-handed, I don’t know; but I love that we’re able to do it in CSS.

The final missing piece is to declare a container on our children. Let’s give the columns a general class name, .grid-item, that we can override if we need to while setting each one as a container we can query for the sake of updating its layout when it is a certain size (as opposed to responding to the viewport’s size in a media query).

:is(.fluid-grid:not(.subgrid-rows),
.repeating-grid:not(.subgrid-rows),
.repeating-flex, .fluid-flex) {
    > * {
    container: var(--grid-item-container, grid-item) / inline-size;
  }
}

That’s a wild-looking selector, but the verbosity is certainly kept to a minimum thanks to the :is() pseudo-class, which saves us from having to write this as a larger chain selector. It essentially selects the direct children of the other utilities without leaking into .subgrid-rows and inadvertently selecting its direct children.

The container property is a shorthand that combines container-name and container-type into a single declaration separated by a forward slash (/). The name of the container is set to one of our variables, and the type is always its inline-size (i.e., width in a horizontal writing mode).

The container-type property can only be applied to grid containers — not grid items. This means we’re unable to combine it with the grid-template-rows: subgrid value, which is why we needed to write a more complex selector to exclude those instances.

Demo

Check out the following demo to see how everything comes together.

See the Pen Grid system playground [forked] by utilitybend.

The demo is pulling in styles from another pen that contains the full CSS for everything we made together in this article. So, if you were to replace the .fluid-flex classname from the parent container in the HTML with another one of the layout utilities, the layout will update accordingly, allowing you to compare them.

Those classes are the following:

  • .repeating-grid,
  • .repeating-flex,
  • .fluid-grid,
  • .fluid-flex.

And, of course, you have the option of turning any grid items into grid containers using the optional .subgrid-rows class in combination with the .repeating-grid and .fluid-grid utilities.

Conclusion: Write Once And Repurpose

This was quite a journey, wasn’t it? It might seem like a lot of information, but we made something that we only need to write once and can use practically anywhere we need a certain type of layout using modern CSS approaches. I strongly believe these utilities can not only help you in a bunch of your work but also cut any reliance on CSS frameworks that you may be using simply for its layout configurations.

This is a combination of many techniques I’ve seen, one of them being a presentation Stephanie Eckles gave at CSS Day 2023. I love it when people handcraft modern CSS solutions for things we used to work around. Stephanie’s demonstration was clean from the start, which is refreshing as so many other areas of web development are becoming ever more complex.

After learning a bunch from CSS Day 2023, I played with Subgrid on my own and published different ideas from my experiments. That’s all it took for me to realize how extensible modern CSS layout approaches are and inspired me to create a set of utilities I could rely on, perhaps for a long time.

By no means am I trying to convince you or anyone else that these utilities are perfect and should be used everywhere or even that they’re better than <framework-du-jour>. One thing that I do know for certain is that by experimenting with the ideas we covered in this article, you will get a solid feel of how CSS is capable of making layout work much more convenient and robust than ever.

Create something out of this, and share it in the comments if you’re willing — I’m looking forward to seeing some fresh ideas!

FabUnit: A Smart Way To Control And Synchronize Typo And Space

What if we were able to implement the sizes of designer’s contribution without hassle? What if we could set custom anchor points to generate a perfectly responsive value, giving us more options as the fluid size approach? What if we had a magic formula that controlled and synchronized the whole project?

It is often the case that I receive two templates from the designer: One for mobile and one for desktop. Lately, I have been asking myself how I could automate the process and optimize the result. How do I implement the specified sizes most effectively? How do I ensure a comfortable tablet view? How can I optimize the output for large screens? How can I react to extreme aspect ratios?

I would like to be able to read the two values of the different sizes (font, spaces, etc.) in pixel and enter them as arguments into a function that does all the work for me. I want to create my own responsive magic formula, my FabUnit.

When I started working on this topic in the spring and launched the FabUnit, I came across this interesting article by Adrian. In the meantime, Ruslan and Brecht have also researched in this direction and presented interesting thoughts.

How Can I Implement The Design Templates Most Effectively?

I am tired of writing media queries for every value, and I want to avoid design jumps. The maintenance and the result are not satisfying. So what is the best way to implement the designer’s contribution?

What about fluid sizes? There are handy calculators like Utopia or Min-Max-Calculator.

But for my projects, I usually need more setting options. The tablet view often turns out too small, and I can neither react to larger viewports nor to the aspect ratio.

And it would be nice to have a proportional synchronization across the whole project. I can define global variables with the calculated values, but I also want to be able to generate an interpolated value locally in the components without any effort. I would like to draw my own responsive line. So I need a tool that spits out the perfect value based on several anchor points (screen definitions) and automates the processes for most of my projects. My tool must be fast and easy to use, and the code must be readable and maintainable.

What Constants From The Design Specifications Should Our Calculations Be Based On?

Let’s take a closer look at our previous example:

The body font size should be 16px on mobile and 22px on desktop (we will deal with the complete style guide later on). The mobile sizes should start at 375px and continuously adjust to 1024px. Up to 1440px, the sizes are to remain statically at the optimum. After that, the values should scale linearly up to 2000px, after which the max-wrapper takes effect.

This gives us the following constants that apply to the whole project:

Xf    375px        global        screen-min        
Xa    1024px        global        screen-opt-start
Xb    1440px        global        screen-opt-end
Xu    2000px        global        screen-max

The body font size should be at least 16px, ideally 22px. The maximum font size at 2000px should be calculated automatically:

Yf    16px        local        size-min        
Ya    22px
Yb    22px        local        size-opt
Yu    auto

So, at the end of the day, my function should be able to take two arguments — in this case, 16 and 22.

fab-unit(16, 22);
The Calculation

If you are not interested in the mathematical derivation of the formula, feel free to jump directly to the section “How To Use The FabUnit?”.

So, let’s get started!

Define Main Clamps

First, we need to figure out which main clamps we want to set.

clamp1: clamp(Yf, slope1, Ya)
clamp2: clamp(Yb, slope2, Yu)

Combine And Nest The Clamps

Now we have to combine the two clamps. This might get a little tricky. We have to consider that the two lines, slope1 and slope2, can overwrite each other, depending on how steep they are. Since we know that slope2 should be 45 degrees or 100% (m = 1), we can query whether slope1 is above 1. This way, we can set a different clamp depending on how the lines intersect.

If slope1 is steeper than slope2, we combine the clamps like this:

clamp(Yf, slope1, clamp(Yb, slope2, Yu))

If slope1 is flatter than slope2, we do this calculation:

clamp(clamp(Yf, slope1, Ya), slope2, Yu)

Combined:

steep-slope
  ? clamp(Yf, slope1, clamp(Yb, slope2, Yu))
  : clamp(clamp(Yf, slope1, Ya), slope2, Yu)

Set The Maximum Wrapper Optionally

What if we don’t have a maximum wrapper that freezes the design above a certain width?

First, we need to figure out which main clamps we want to set.

clamp1: clamp(Yf, slope1, Ya)
max: max(Yb, slope2)

If slope1 is steeper than slope2:

clamp(Yf, slope1, max(Yb, slope2))

If slope1 is flatter than slope2:

max(clamp(Yf, slope1, Ya), slope2)

The calculation without wrapper — elastic upwards:

steep-slope
  ? clamp(Yf, slope1, max(Yb, slope2))
  : max(clamp(Yf, slope1, Ya), slope2)

Combined, with optional max wrapper (if screen-max Xu is set):

Xu
  ? steep-slope
    ? clamp(Yf, slope1, clamp(Yb, slope2, Yu))
    : max(clamp(Yf, slope1, Ya), Yu)
  : steep-slope
    ? clamp(Yf, slope1, max(Yb, slope2))
    : max(clamp(Yf, slope1, Ya), slope2)

Thus we have built the basic structure of the formula. Now we dive a little deeper.

Calculate The Missing Values

Let’s see which values we get in as an argument and which we have to calculate now:

steep-slope
  ? clamp(Yf, slope1, clamp(Yb, slope2, Yu))
  : max(clamp(Yf, slope1, Ya), Yu)

steep-slope
Ya = Yb = 22px
Yf = 16px
slope1 = Mfa
slope2 = Mbu
Yu

  • steep-slope
    Check whether the slope Yf → Ya is above the slope Yb → Yu (m = 1) :
((Ya - Yf) / (Xa - Xf)) * 100 > 1
  • Mfa
    Linear interpolation, including the calculation of the slope Yf → Ya:
Yf + (Ya - Yf) * (100vw - Xf) / (Xa - Xf)
  • Mbu
    Linear interpolation between Yb and Yu (slope m = 1):
100vw / Xb * Yb
  • Yu
    Calculating the position of Yu:
(Xu / Xb) * Yb

Put All Together

Xu
  ? ((Ya - Yf) / (Xa - Xf)) * 100 > 1
    ? clamp(Yf, Yf + (Ya - Yf) * (100vw - Xf) / (Xa - Xf), clamp(Yb, 100vw / Xb * Yb, (Xu / Xb) * Yb))
    : max(clamp(Yf, Yf + (Ya - Yf) * (100vw - Xf) / (Xa - Xf), Ya), (Xu / Xb) * Yb)
  : ((Ya - Yf) / (Xa - Xf)) * 100 > 1
    ? clamp(Yf, Yf + (Ya - Yf) * (100vw - Xf) / (Xa - Xf), max(Yb, 100vw / Xb * Yb))
    : max(clamp(Yf, Yf + (Ya - Yf) * (100vw - Xf) / (Xa - Xf), Ya), 100vw / Xb * Yb)

We’d better store some calculations in variables:

steep-slope = ((Ya - Yf) / (Xa - Xf)) * 100 > 1
slope1 = Yf + (Ya - Yf) * (100vw - Xf) / (Xa - Xf)
slope2 = 100vw / Xb * Yb
Yu = (Xu / Xb) * Yb

Xu
  ? steep-slope
    ? clamp(Yf, slope1, clamp(Yb, slope2, Yu))
    : max(clamp(Yf, slope1, Ya), Yu)
  : steep-slope
    ? clamp(Yf, slope1, max(Yb, slope2))
    : max(clamp(Yf, slope1, Ya), slope2)
Include Aspect Ratio

Because we now see how the cat jumps, we treat ourselves to another cookie. In the case of an extremely wide format, e.g. mobile device landscape, we want to scale down the sizes again. It’s more pleasant and readable this way.

So what if we could include the aspect ratio in our calculations? In this example, we want to shrink the sizes when the screen is wider than the aspect ratio of 16:9.

aspect-ratio = 16 / 9
screen-factor = min(100vw, 100vh * aspect-ratio)

In both slope interpolations, we simply replace 100vw with the new screen factor.

slope1 = Yf + (Ya - Yf) * (screen-factor - Xf) / (Xa - Xf)
slope2 = screen-factor / Xb * Yb

So, finally, that’s it. Let’s look at the whole magic formula now.

Formula
screen-factor = min(100vw, 100vh * aspect-ratio)
steep-slope = ((Ya - Yf) / (Xa - Xf)) * 100 > 1
slope1 = Yf + (Ya - Yf) * (screen-factor - Xf) / (Xa - Xf)
slope2 = screen-factor / Xb * Yb
Yu = (Xu / Xb) * Yb

Xu
  ? steep-slope
    ? clamp(Yf, slope1, clamp(Yb, slope2, Yu))
    : max(clamp(Yf, slope1, Ya), Yu)
  : steep-slope
    ? clamp(Yf, slope1, max(Yb, slope2))
    : max(clamp(Yf, slope1, Ya), slope2)

Function

Now we can integrate the formula into our setup. In this article, we’ll look at how to implement it in Sass. The two helper functions ensure that we output the rem values correctly (I will not go into it in detail). Then we set the anchor points and the aspect ratio as constants (respectively, Sass variables). Finally, we replace the coordinate points of our formula with variable names, and the FabUnit is ready for use.

_fab-unit.scss

@use "sass:math";


/* Helper functions */

$rem-base: 10px;

@function strip-units($number) {
  @if (math.is-unitless($number)) {
      @return $number;
    } @else {
      @return math.div($number, $number * 0 + 1);
  }
}

@function rem($size){
  @if (math.compatible($size, 1rem) and not math.is-unitless($size)) {
    @return $size;
  } @else {
    @return math.div(strip-units($size), strip-units($rem-base)) * 1rem;
  }
}


/* Default values fab-unit 🪄 */

$screen-min: 375;
$screen-opt-start: 1024;
$screen-opt-end: 1440;
$screen-max: 2000;  // $screen-opt-end | int > $screen-opt-end | false
$aspect-ratio: math.div(16, 9);  // smaller values for larger aspect ratios


/* Magic function fab-unit 🪄 */

@function fab-unit(
    $size-min, 
    $size-opt, 
    $screen-min: $screen-min, 
    $screen-opt-start: $screen-opt-start, 
    $screen-opt-end: $screen-opt-end, 
    $screen-max: $screen-max,
    $aspect-ratio: $aspect-ratio
  ) {
  $screen-factor: min(100vw, 100vh  $aspect-ratio);
  $steep-slope: math.div(($size-opt - $size-min), ($screen-opt-start - $screen-min))  100 > 1;
  $slope1: calc(rem($size-min) + ($size-opt - $size-min)  ($screen-factor - rem($screen-min)) / ($screen-opt-start - $screen-min));
  $slope2: calc($screen-factor / $screen-opt-end  $size-opt);
  @if $screen-max {
    $size-max: math.div(rem($screen-max), $screen-opt-end) * $size-opt;
    @if $steep-slope {
      @return clamp(rem($size-min), $slope1, clamp(rem($size-opt), $slope2, $size-max));
    } @else {
      @return clamp(clamp(rem($size-min), $slope1, rem($size-opt)), $slope2, $size-max);
    }
  } @else {
    @if $steep-slope {
      @return clamp(rem($size-min), $slope1, max(rem($size-opt), $slope2));
    } @else {
      @return max(clamp(rem($size-min), $slope1, rem($size-opt)), $slope2);
    }
  }
}
How To Use The FabUnit?

The work is done, now it’s simple. The style guide from our example can be implemented in no time:

We read the related values from the style guide and pass them to the FabUnit as arguments: fab-unit(16, 22).

style.scss

@import "fab-unit";


/* overwrite default values 🪄 */
$screen-max: 1800;


/* Style guide variables fab-unit 🪄 */

$fab-font-size-body: fab-unit(16, 22);
$fab-font-size-body-small: fab-unit(14, 16);

$fab-font-size-h1: fab-unit(60, 160);
$fab-font-size-h2: fab-unit(42, 110);
$fab-font-size-h3: fab-unit(28, 60);

$fab-space-s: fab-unit(20, 30);
$fab-space-m: fab-unit(40, 80);
$fab-space-l: fab-unit(60, 120);
$fab-space-xl: fab-unit(80, 180);


/* fab-unit in action 🪄 */

html {
  font-size: 100% * math.div(strip-units($rem-base), 16);
}

body {
  font-size: $fab-font-size-body;
}

.wrapper {
  max-width: rem($screen-max);
  margin-inline: auto;
  padding: $fab-space-m;
}

h1 {
  font-size: $fab-font-size-h1;
  border-block-end: fab-unit(2, 10) solid plum;
}

…

p {
  margin-block: $fab-space-s;
}

…
/* other use cases for calling fab-unit 🪄 */

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(fab-unit(200, 500), 1fr));
  gap: $fab-space-m;
}

.thing {
  flex: 0 0 fab-unit(20, 30);
  height: fab-unit(20, 36, 660, 800, 1600, 1800);  /* min, opt, … custom anchor points */
}

We are now able to draw the responsive line by calling fab-unit() and specifying just two sizes, the minimum and the optimum. We can control the font sizes, paddings, margins and gaps, heights and widths, and even — if we want to — define grid columns and flex layouts with it. We are also able to move the predefined anchor points locally.

Let’s have a look at the compiled output:

…
font-size: clamp(clamp(1.3rem, 1.3rem + 2 * (min(100vw, 177.7777777778vh) - 37.5rem) / 649, 1.5rem), min(100vw, 177.7777777778vh) / 1440 * 15, 2.0833333333rem);
…

And the computed output:

font-size: 17.3542px

Accessibility Concerns

To ensure good accessibility, I recommend testing in each case whether all sizes are sufficiently zoomable. Arguments with a large difference might not behave as desired. For more information on this topic, you can check the article “Responsive Type and Zoom” by Adrian Roselli.

Conclusion

Now we have created a function that does all the work for us. It takes a minimum and an optimum value and spits out a calculation to our CSS property, considering the screen width, aspect ratio, and the specified anchor points — a single formula that drives the entire project. No media queries, no breakpoints, no design jumps.

The FabUnit presented here is based on my own experience and is optimized for most of my projects. I save a lot of time and am satisfied with the result. It may be that you and your team have another approach and therefore have other requirements for a FabUnit. It would be nice if you were now able to create your own FabUnit according to your needs.

I would be happy if my approach inspired you with new ideas. I would be honored if you directly use the npm package of the FabUnit from this article for your projects.

Thank you! 🙏🏻

FAB-UNIT LINKS

Merci Eli, Roman, Patrik, Fidi.

Exploring CSS Grid’s Implicit Grid and Auto-Placement Powers

When working with CSS Grid, the first thing to do is to set display: grid on the element that we want to be become a grid container. Then we explicitly define the grid using a combination of grid-template-columns, grid-template-rows, and grid-template-areas. And from there, the next step is to place items inside the grid.

This is the classic approach that should be used and I also recommend it. However, there is another approach for creating grids without any explicit definition. We call this the implicit grid.

“Explicit, implicit? What the heck is going on here?”

Strange terms, right? Manuel Matuzovic already has a good explanation of what we may by “implicit” and “explicit” in CSS Grid, but let’s dig straight into what the specification says:

The grid-template-rows, grid-template-columns, and grid-template-areas properties define a fixed number of tracks that form the explicit grid. When grid items are positioned outside of these bounds, the grid container generates implicit grid tracks by adding implicit grid lines to the grid. These lines together with the explicit grid form the implicit grid.

So, in plain English, the browser auto-generates extra rows and columns in case any elements happen to be placed outside the defined grid.

What about auto-placement?

Similar to the concept of implicit grid, auto-placement is the ability of the browser to automatically place the items inside the grid. We don’t always need to give the position of each item.

Through different use cases, we are going to see how such features can help us create complex and dynamic grid with a few lines of code.

Dynamic sidebar

Here, we have three different layouts but we only have one grid configuration that works for all of them.

main {
  display: grid;
  grid-template-columns: 1fr;
}

Only one column is taking up all the free space. This is our “explicit” grid. It’s set up to fit one grid item in the main grid container. That’s all. One column and one row:

But what if we decided to drop another element in there, say an aside (our dynamic sidebar). As it’s currently (and explicitly) defined, our grid will have to adjust automatically to find a place for that element. And if we do nothing else with our CSS, here’s what DevTools tells us is happening.

The element takes up the entire column that is explicitly set on the container. Meanwhile, the falls onto a new row between implicit grid lines labeled 2 and 3. Note that I’m using a 20px gap to help separate things visually.

We can move the <aside> to a column beside the <section>:

aside {
  grid-column-start: 2;
}

And here’s what DevTools tells us now:

The element is between the grid container’s first and second grid column lines. The starts at the second grid column line and ends at a third line we never declared.

We place our element in the second column but… we don’t have a second column. Weird, right? We never declared a second column on the <main> grid container, but the browser created one for us! This is the key part from the specification we looked at:

When grid items are positioned outside of these bounds, the grid container generates implicit grid tracks by adding implicit grid lines to the grid.

This powerful feature allows us to have dynamic layouts. If we only have the <section> element, all we get is one column. But if we add an <aside> element to the mix, an extra column is created to contain it.

We could place the <aside> before the <section> instead like this:

aside {
  grid-column-end: -2;
} 

This creates the implicit column at the start of the grid, unlike the previous code that places the implicit column at the end.

We can have either a right or left sidebar

We can do the same thing more easily using the grid-auto-flow property to set any and all implicit tracks to flow in a column direction:

Now there’s no need to specify grid-column-start to place the <aside> element to the right of the <section>! In fact, any other grid item we decide to throw in there at any time will now flow in a column direction, each one placed in its own implicit grid tracks. Perfect for situations where the number of items in the grid isn’t known in advance!

That said, we do still need grid-column-end if we want to place it in a column to the left of it because, otherwise, the <aside> will occupy the explicit column which, in turn, pushes the <section> outside the explicit grid and forces it to take the implicit column.

I know, I know. That’s a little convoluted. Here is another example we can use to better understand this little quirk:

In the first example, we didn’t specify any placement. In this case, the browser will first place the <aside> element in the explicit column since it comes first in the DOM. The <section>, meanwhile, is automatically placed in the grid column the browser automatically (or implicitly) creates for us.

In the second example, we set the <aside> element outside of the explicit grid:

aside {
  grid-column-end: -2;
}

Now it doesn’t matter that <aside> comes first in the HTML. By reassigning <aside> somewhere else, we’ve made the <section> element available to take the explicit column.

Image grid

Let’s try something different with a grid of images where we have a big image and a few thumbnails beside it (or under it).

We have two grid configurations. But guess what? I am not defining any grid at all! All I am doing is this:

.grid img:first-child {
  grid-area: span 3 / span 3;
}

It’s surprising we only need one line of code to pull off something like this, so let’s dissect what’s going on and you will see that it’s easier than you may think. First of all, grid-area is a shorthand property that combines the following properties into a single declaration:

  • grid-row-start
  • grid-row-end
  • grid-column-start
  • grid-column-end

Wait! Isn’t grid-area the property we use to define named areas instead of where elements start and end on the grid?

Yes, but it also does more. We could write a whole lot more about grid-area, but in this particular case:

.grid img:first-child {
  grid-area: span 3 / span 3;
}

/* ...is equivalent to: */
.grid img:first-child {
  grid-row-start: span 3;
  grid-column-start: span 3;
  grid-row-end: auto;
  grid-column-end: auto;
}

We can see the same thing when cracking open DevTools to expand the shorthand version:

This means that the first image element in the grid needs to span three columns and three rows. But since we didn’t define any columns or rows, the browser does it for us.

We’ve essentially placed the first image in the HTML to take up a 3⨉3 grid. That means that any other images will be placed automatically in those same three columns without the need to specify anything new.

To summarize, we told the browser that the first image needs take up the space of three columns and three rows that we never explicitly defined when setting up the grid container. The browser set those columns and rows up for us. As a result, the remaining images in the HTML flow right into place using the same three columns and rows. And since the first image takes up all three columns in the first row, the remaining images flow into additional rows that each contain three columns, where each image takes up a single column.

All this from one line of CSS! That’s the power of “implicit” grid” and auto-placement.

For the second grid configuration in that demo, all I’ve done is change the automatic flow direction using grid-auto-flow: column the same way we did earlier when placing an <aside> element next to a <section>. This forces the browser to create a fourth column it can use to place the remaining images. And since we have three rows, the remaining images get placed inside the same vertical column.

We need to add a few properties to the images to make sure they fit nicely inside the grid without any overflow:

.grid {
  display: grid;
  grid-gap: 10px;
}

/* for the second grid configuration */
.horizontal {
  grid-auto-flow: column;
}

/* The large 3⨉3 image */
.grid img:first-child {
  grid-area: span 3 / span 3;
}

/* Help prevent stretched or distorted images */
img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

And of course, we can easily update the grid to consider more images by adjusting one value. That would be the 3 in the styles for the large image. We have this:

.grid img:first-child {
  grid-area: span 3 / span 3;
}

But we could add a fourth column simply by changing it to 4 instead:

.grid img:first-child {
  grid-area: span 4 / span 4;
}

Even better: let’s set that up as a custom property to make things even easier to update.

Dynamic layouts

The first use case with the sidebar was our first dynamic layout. Now we will tackle more complex layouts where the number of elements will dictate the grid configuration.

In this example, we can have anywhere from one to four elements where the grid adjusts in way that nicely fits the number of elements without leaving any awkward gaps or missing spaces.

When we have one element, we do nothing. The element will stretch to fill the only row and column automatically created by the grid.

Bit when we add the second element, we create another (implicit) column using grid-column-start: 2.

When we add a third element, it should take up the width of two columns — that’s why we used grid-column-start: span 2, but only if it’s the :last-child because if (and when) we add a fourth element, that one should only take up a single column.

Adding that up, we have four grid configurations with only two declarations and the magic of implicit grid:

.grid {
  display: grid;
}
.grid :nth-child(2) {
  grid-column-start: 2;
}
.grid :nth-child(3):last-child {
  grid-column-start: span 2;
}

Let’s try another one:

We’re doing nothing for the first and second cases where we have only one or two elements. When we add a third element, though, we tell the browser that — as long as it’s the :last-child — it should span two columns. When we add a fourth element, we tell the browser that element needs to be placed in the second column.

.grid {
  display: grid;
}
.grid :nth-child(3):last-child {
  grid-column-start: span 2;
}
.grid :nth-child(4) {
  grid-column-start: 2;
}

Are you starting to get the trick? We give the browser specific instructions based on the number of elements (using :nth-child) and, sometimes, one instruction can change the layout completely.

It should be noted that the sizing will not be the same when we work with different content:

Since we didn’t define any sizes for our items, the browser automatically sizes them for us based on their contents and we may end up with different sizing than what we just saw. To overcome this, we have to explicitly specify that all the columns and rows are equally sized:

grid-auto-rows: 1fr;
grid-auto-columns: 1fr;

Hey, we haven’t played with those properties yet! grid-auto-rows and grid-auto-columns set the size of implicit rows and columns, respectively, in a grid container. Or, as the spec explains it:

The grid-auto-columns and grid-auto-rows properties specify the size of tracks not assigned a size by grid-template-rows or grid-template-columns.

Here is another example where we can go up to six elements. This time I will let you dissect the code. Don’t worry, the selectors may look complex but the logic is pretty straightforward.

Even with six elements, we only needed two declarations. Imagine all the complex and dynamic layouts we can achieve with a few lines of code!

What’s going on with that grid-auto-rows and why does it take three values? Are we defining three rows?

No, we are not defining three rows. But we are defining three values as a pattern for our implicit rows. The logic is as follows:

  • If we have one row, it will get sized with the first value.
  • If we have two rows, the first one gets the first value and the second one the second value.
  • If we have three rows, the three values will get used.
  • If we have four rows (and here comes the interesting part), we use the three values for the first three rows and we reuse the first value again for the fourth row. That’s why it’s a kind of pattern that we repeat to size all the implicit rows.
  • If we have 100 rows, they will be sized three-by-three to have 2fr 2fr 1fr 2fr 2fr 1fr 2fr 2fr 1fr, etc.

Unlike grid-template-rows which defines the number of rows and their sizes, grid-auto-rows only sizes row that may get created along the way.

If we get back to our example, the logic is to have equal size when two rows are created (we will use the 2fr 2fr), but if a third row is created we make it a bit smaller.

Grid patterns

For this last one, we are going to talk about patterns. You have probably seen those two column layouts where one column is wider than the other, and each row alternates the placement of those columns.

This sort layout can be difficult too pull off without knowing exactly how much content we’re dealing with, but CSS Grid’s auto-placement powers makes it a relative cinch.

Take a peek at the code. It may look complex but let’s break it down because it winds up being pretty straightforward.

The first thing to do is to identify the pattern. Ask yourself: “After how many elements should the pattern repeat?” In this case it’s after every four elements. So, let’s look at using only four elements for now:

Now, let’s define the grid and set up the general pattern using the :nth-child selector for alternating between elements:

.grid {
  display: grid;
  grid-auto-columns: 1fr; /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows equal to 100px */
}
.grid :nth-child(4n + 1) { /* ?? */ }
.grid :nth-child(4n + 2) { /* ?? */ }
.grid :nth-child(4n + 3) { /* ?? */ }
.grid :nth-child(4n + 4) { /* ?? */ }

We said that our pattern repeats every four elements, so we will logically use 4n + x where x ranges from 1 to 4. It’s a little easier to explain the pattern this way:

4(0) + 1 = 1 = 1st element /* we start with n = 0 */
4(0) + 2 = 2 = 2nd element
4(0) + 3 = 3 = 3rd element
4(0) + 4 = 4 = 4th element
4(1) + 1 = 5 = 5th element /* our pattern repeat here at n = 1 */
4(1) + 2 = 6 = 6th element
4(1) + 3 = 7 = 7th element
4(1) + 4 = 8 = 8th element
4(2) + 1 = 9 = 9th element /* our pattern repeat again here at n = 2 */
etc.

Perfect, right? We have four elements, and repeat the pattern on the fifth element, the ninth element and so on.

Those :nth-child selectors can be tricky! Chris has a super helpful explanation of how it all works, including recipes for creating different patterns.

Now we configure each element so that:

  1. The first element needs to take two columns and start at column one (grid-column: 1/span 2).
  2. The second element is placed in the third column (grid-column-start: 3).
  3. The third element is placed at the first column: (grid-column-start: 1).
  4. The fourth element takes two columns and starts at the second column: (grid-column: 2/span 2).

Here that is in CSS:

.grid {
  display: grid;
  grid-auto-columns: 1fr; /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
}
.grid :nth-child(4n + 1) { grid-column: 1/span 2; }
.grid :nth-child(4n + 2) { grid-column-start: 3; }
.grid :nth-child(4n + 3) { grid-column-start: 1; }
.grid :nth-child(4n + 4) { grid-column: 2/span 2; }

We could stop here and be done… but we can do better! Specifically, we can remove some declarations and rely grid’s auto-placement powers to do the job for us. This is the trickiest part to grok and requires a lot of practice to be able to identify what can be removed.

The first thing we can do is update grid-column: 1 /span 2 and use only grid-column: span 2 since, by default, the browser will place the first item into the first column. We can also remove this:

.grid :nth-child(4n + 3) { grid-column-start: 1; }

By placing the first, second, and fourth items, the grid automatically places the third item in the correct place. That means we’re left with this:

.grid {
  display: grid;
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
  grid-auto-columns: 1fr; /* all the columns are equal */
}
.grid :nth-child(4n + 1) { grid-column: span 2; }
.grid :nth-child(4n + 2) { grid-column-start: 3; }
.grid :nth-child(4n + 4) { grid-column: 2/span 2; }

But c’mon we can stroll do better! We can also remove this:

.grid :nth-child(4n + 2) { grid-column-start: 2; }

Why? If we place the fourth element in the second column while allowing it to take up two full columns, we’re forcing the grid to create a third implicit column, giving us a total of three columns without explicitly telling it to. The fourth element cannot go into the first row since the first item is also taking two columns, so it flows to the next row. This configuration leave us with an empty column in the first row and an empty one in the second row.

I think you know the end of the story. The browser will automatically place the second and third items in those empty spots. So our code becomes even simpler:

.grid {
  display: grid;
  grid-auto-columns: 1fr; /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
}
.grid :nth-child(4n + 1) { grid-column: span 2; }
.grid :nth-child(4n + 4) { grid-column: 2/span 2; }

All it takes is five declarations to create a very cool and very flexible pattern. The optimization part may be tricky, but you get used to it and gain some tricks with practice.

Why not use grid-template-columns to define explicit columns since we know the number of columns?

We can do that! Here’s the code for it:

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* all the columns are equal */
  grid-auto-rows: 100px; /* all the rows are equal to 100px */
}
.grid :nth-child(4n + 1),
.grid :nth-child(4n + 4) {
  grid-column: span 2;
}

As you can see, the code is definitely more intuitive. We define three explicit grid columns and we tell the browser that the first and fourth elements need to take two columns. I highly recommend this approach! But the goal of this article is to explore new ideas and tricks that we get from CSS Grid’s implicit and auto-placement powers.

The explicit approach is more straightforward, while an implicit grid requires you to — pardon the pun — fill in the gaps where CSS is doing additional work behind the scenes. In the end, I believe that having a solid understanding of implicit grids will help you better understand the CSS Grid algorithm. After all, we are not here to study what’s obvious — we are here to explore wild territories!

Let’s try another pattern, a bit quicker this time:

Our pattern repeats every six elements. The third and fourth elements each need to occupy two full rows. If we place the third and the fourth elements, it seems that we don’t need to touch the others, so let’s try the following:

.grid {
  display: grid;
  grid-auto-columns: 1fr;
  grid-auto-rows: 100px;
}
.grid :nth-child(6n + 3) {
  grid-area: span 2/2; /* grid-row-start: span 2 && grid-column-start: 2 */
}
.grid :nth-child(6n + 4) {
  grid-area: span 2/1; /* grid-row-start: span 2 && grid-column-start: 1 */
}

Hmm, no good. We need to place the second element in the first column. Otherwise, the grid will automatically place it in the second column.

.grid :nth-child(6n + 2) {
  grid-column: 1; /* grid-column-start: 1 */
}

Better, but there’s still more work, We need to shift the third element to the top. It’s tempting to try placing it in the first row this way:

.grid :nth-child(6n + 3) {
  grid-area: 1/2/span 2; 
    /* Equivalent to:
       grid-row-start: 1;
       grid-row-end: span 2;
       grid-column-start: 2 
     */
}

But this doesn’t work because it forces all the 6n + 3 elements to get placed in the same area which makes a jumbled layout. The real solution is to keep the initial definition of the third element and add grid-auto-flow: dense to fill the gaps. From MDN:

[The] “dense” packing algorithm attempts to fill in holes earlier in the grid, if smaller items come up later. This may cause items to appear out-of-order, when doing so would fill in holes left by larger items. If it is omitted, a “sparse” algorithm is used, where the placement algorithm only ever moves “forward” in the grid when placing items, never backtracking to fill holes. This ensures that all of the auto-placed items appear “in order”, even if this leaves holes that could have been filled by later items.

I know this property is not very intuitive but never forget it when you face a placement issue. Before trying different configurations in vain, add it because it may fix your layout with no additional effort.

Why not always add this property by default?

I don’t recommend it because, in some cases, we don’t want that behavior. Note how the MDN’s explanation there mentions it causes items to flow “out-of-order” to fill holes left by larger items. Visual order is usually just as important as the source order, particularly when it comes to accessible interfaces, and grid-auto-flow: dense can sometimes cause a mismatch between the visual and source order.

Our final code is then:

.grid {
  display: grid;
  grid-auto-columns: 1fr;
  grid-auto-flow: dense;
  grid-auto-rows: 100px;
}
.grid :nth-child(6n + 2) { grid-column: 1; }
.grid :nth-child(6n + 3) { grid-area: span 2/2; }
.grid :nth-child(6n + 4) { grid-row: span 2; }

Another one? Let’s go!

For this one, I will not talk too much and instead show you an illustration of the code I have used. Try to see if you get how I reached that code:

The items in black are implicitly placed in the grid. It should be noted that we can get the same layout more ways than how I got there. Can you figure those out, too? What about using grid-template-columns? Share your works in the comment section.

I am gonna leave you with a last pattern:

I do have a solution for this one but it’s your turn to practice. Take all that we have learned and try to code this by yourself and then compare it with my solution. Don’t worry if you end with something verbose — the most important thing is finding a working solution.

Want more?

Before we end I want to share a few Stack Overflow questions related to CSS Grid where I jumped in with answers that use many of the techniques we covered here together. It’s a good list that shows just how many real use cases and real-world situations come up where these things come in handy:

Wrapping up

CSS Grid has been around for years, but there are still a lot of little-known and used tricks that aren’t widely discussed. The implicit grid and auto-placement features are two of them!

And yes, this can get challenging! It has taken me a lot of time to grok the logic behind implicit grids and I still struggle with auto-placement. If you want to spend more time wrapping your head around explicit and implicit grids, here are a couple of additional explanations and examples worth checking out:

Similarly, you might want to read about grid-auto-columns in the CSS-Tricks Almanac because Mojtaba Seyedi goes into great detail and includes incredibly helpful visuals to help explain the behavior.

Like I said when we started, the methods we covered here are not meant to replace the common ways you already know for building grids. I am simply exploring different ways that can be helpful in some cases.


Exploring CSS Grid’s Implicit Grid and Auto-Placement Powers originally published on CSS-Tricks. You should get the newsletter.