16

Is it possible to define a grid with a maximum number of columns, but allow elements to wrap onto new rows when the screen width changes?

I have implemented classes that allow rows to wrap onto new rows, but there is no maximum number of columns.

Here is a CodePen of one method using Flexbox:

CSS:

.flex-container {
  display: flex;
  flex-wrap: wrap;
}

Another method is to use a grid:

.grid {
  display: grid;
  counter-reset: grid-items;
  position: relative;
}


.grid--auto-fit {
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

I want a fairly generic solution to this. Is what I want to achieve possible without JavaScript or media queries?

Peter Mortensen
  • 30,030
  • 21
  • 100
  • 124
Ben Allen
  • 171
  • 1
  • 9
  • have you tried : `grid-template-columns: repeat(auto-fit, minmax(25%, 1fr));` for a four columns max ? or 12.5% for eight an so on ? – G-Cyrillus Mar 21 '19 at 14:20

3 Answers3

14

With CSS grid you can consider the use of max(width, 100%/N) where N is the maximum number of columns. If the width of the container increases, 100%/N will for sure be bigger than width, thus we won't have more than N elements per row.

.grid-container {
  --n: 4; /* The maximum number of columns */
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(max(200px, 100%/var(--n)), 1fr));
}

.grid-item {
  background: tomato;
  padding: 5px;
  height: 50px;
  margin: 10px;

  line-height: 50px;
  color: white;
  font-weight: bold;
  font-size: 2em;
  text-align: center;
  box-sizing: border-box;
}
<div class="grid-container">
  <div class="grid-item">1</div>
  <div class="grid-item">2</div>
  <div class="grid-item">3</div>
  <div class="grid-item">4</div>
  <div class="grid-item">5</div>
  <div class="grid-item">6</div>
  <div class="grid-item">7</div>
  <div class="grid-item">8</div>
</div>

<div class="grid-container" style="--n:3">
  <div class="grid-item">1</div>
  <div class="grid-item">2</div>
  <div class="grid-item">3</div>
  <div class="grid-item">4</div>
  <div class="grid-item">5</div>
  <div class="grid-item">6</div>
  <div class="grid-item">7</div>
  <div class="grid-item">8</div>
</div>

With gaps:

.grid-container {
  --n: 4; /* The maximum number of columns */
  display: grid;
  grid-template-columns: repeat(auto-fill,
           minmax(max(200px,(100% - (var(--n) - 1)*10px)/var(--n)), 1fr));
  gap: 10px;
  margin: 5px;
}

.grid-item {
  background: tomato;
  padding: 5px;
  height: 50px;

  line-height: 50px;
  color: white;
  font-weight: bold;
  font-size: 2em;
  text-align: center;
  box-sizing: border-box;
}
<div class="grid-container">
  <div class="grid-item">1</div>
  <div class="grid-item">2</div>
  <div class="grid-item">3</div>
  <div class="grid-item">4</div>
  <div class="grid-item">5</div>
  <div class="grid-item">6</div>
  <div class="grid-item">7</div>
  <div class="grid-item">8</div>
</div>

<div class="grid-container" style="--n:3">
  <div class="grid-item">1</div>
  <div class="grid-item">2</div>
  <div class="grid-item">3</div>
  <div class="grid-item">4</div>
  <div class="grid-item">5</div>
  <div class="grid-item">6</div>
  <div class="grid-item">7</div>
  <div class="grid-item">8</div>
</div>

With flexbox, you can simply set a max-width to the container since your elements have a fixed width:

.flex-container {
  display: flex;
  flex-wrap: wrap;
  max-width: calc(5*(200px + 20px));
}

.flex-item {
  background: tomato;
  padding: 5px;
  width: 200px;
  height: 100px;
  margin: 10px;

  line-height: 100px;
  color: white;
  font-weight: bold;
  font-size: 2em;
  text-align: center;
  box-sizing: border-box;
}
<div class="flex-container wrap">
  <div class="flex-item">1</div>
  <div class="flex-item">2</div>
  <div class="flex-item">3</div>
  <div class="flex-item">4</div>
  <div class="flex-item">5</div>
  <div class="flex-item">6</div>
  <div class="flex-item">7</div>
  <div class="flex-item">8</div>
</div>

The only drawback is that you need to know the width of your elements and their margin to correctly set the max-width.

If you want your elements to expand and cover all the width, you can use a trick with min-width like below:

.flex-container {
  display: flex;
  flex-wrap: wrap;
}

.flex-item {
  background: tomato;
  padding: 5px;
  min-width: 200px;
  width: calc(100%/5 - 20px); /* 5 columns */
  height: 100px;
  margin: 10px;

  line-height: 100px;
  color: white;
  font-weight: bold;
  font-size: 2em;
  text-align: center;
  box-sizing: border-box;
}
<div class="flex-container wrap">
  <div class="flex-item">1</div>
  <div class="flex-item">2</div>
  <div class="flex-item">3</div>
  <div class="flex-item">4</div>
  <div class="flex-item">5</div>
  <div class="flex-item">6</div>
  <div class="flex-item">7</div>
  <div class="flex-item">8</div>
</div>

Here also you need to consider the margin. You can easily make this more flexible using CSS variables:

.flex-container {
  display: flex;
  flex-wrap: wrap;
}

.flex-item {
  --m: 10px;
  background: tomato;
  padding: 5px;
  min-width: 200px;
  width: calc(100%/5 - 2*var(--m)); /* 5 columns */
  height: 100px;
  margin: var(--m);

  line-height: 100px;
  color: white;
  font-weight: bold;
  font-size: 2em;
  text-align: center;
  box-sizing: border-box;
}
<div class="flex-container wrap">
  <div class="flex-item">1</div>
  <div class="flex-item">2</div>
  <div class="flex-item">3</div>
  <div class="flex-item">4</div>
  <div class="flex-item">5</div>
  <div class="flex-item">6</div>
  <div class="flex-item">7</div>
  <div class="flex-item">8</div>
</div>

You can also consider flex-grow if you want your element to always expand (even when there is a wrap), but you may face the issue of the last row that you need to fix with some hacks:

.flex-container {
  display: flex;
  flex-wrap: wrap;
  --m: 10px;
}

.flex-item {
  background: tomato;
  padding: 5px;
  min-width: 200px;
  flex-grow: 1;
  width: calc(100%/5 - 2*var(--m)); /* 5 columns */
  height: 100px;
  margin: var(--m);

  line-height: 100px;
  color: white;
  font-weight: bold;
  font-size: 2em;
  text-align: center;
  box-sizing: border-box;
}

.flex-container span {
  min-width: 200px;
  flex-grow: 1;
  width: calc(100%/5 - 2*var(--m)); /* 5 columns */
  margin: 0 var(--m);
}
<div class="flex-container wrap">
  <div class="flex-item">1</div>
  <div class="flex-item">2</div>
  <div class="flex-item">3</div>
  <div class="flex-item">4</div>
  <div class="flex-item">5</div>
  <div class="flex-item">6</div>
  <div class="flex-item">7</div>
  <div class="flex-item">8</div>

  <!-- 4 empty elements to fix the issue (we can also use a pseudo element) -->
  <span></span>
  <span></span>
  <span></span>
  <span></span>
</div>

In the example below, we made the number of columns to be 5 so we will need at least 4 empty elements to fix the issue in case we will have one to 4 elements in the last row.

Of course, this is a drawback, but since you know the number of columns you can easily set those empty elements and you won't need any JavaScript.

To make it more flexible, here is an idea with CSS variables:

.flex-container {
  display: flex;
  flex-wrap: wrap;
  border: 1px solid;
  --m: 10px;
  --n: 5;
  --width: 150px;
}

.flex-item {
  background: tomato;
  min-width: var(--width);
  flex-grow: 1;
  width: calc(100%/var(--n) - 2*var(--m));
  height: 50px;
  margin: var(--m);

  box-sizing: border-box;
}

.flex-container span {
  display: contents; /* Each span will give us 2 elements */
}
.flex-container span: before,
.flex-container span: after,
.flex-container: before,
.flex-container: after{
  content: "";
  min-width: var(--width);
  flex-grow: 1;
  width: calc(100%/var(--n) - 2*var(--m));
  margin :0 var(--m);
  order: 1; /* We make sure they are at the end */
}
<div class="flex-container wrap">
  <div class="flex-item">1</div>
  <div class="flex-item">2</div>
  <div class="flex-item">3</div>
  <div class="flex-item">4</div>
  <div class="flex-item">5</div>
  <div class="flex-item">6</div>
  <div class="flex-item">7</div>
  <div class="flex-item">8</div>

  <!-- A lot of elements !! -->
  <span></span>
  <span></span>
  <span></span>
  <span></span>
  <span></span>
  <span></span>
  <span></span>
</div>

<div class="flex-container wrap" style="--n:10">
  <div class="flex-item">1</div>
  <div class="flex-item">2</div>
  <div class="flex-item">3</div>
  <div class="flex-item">4</div>
  <div class="flex-item">5</div>
  <div class="flex-item">6</div>
  <div class="flex-item">7</div>
  <div class="flex-item">8</div>

  <!-- A lot of elements !! -->
  <span></span>
  <span></span>
  <span></span>
  <span></span>
  <span></span>
  <span></span>
  <span></span>
</div>

<div class="flex-container wrap" style="--n:3">
  <div class="flex-item">1</div>
  <div class="flex-item">2</div>
  <div class="flex-item">3</div>
  <div class="flex-item">4</div>
  <div class="flex-item">5</div>
  <div class="flex-item">6</div>
  <div class="flex-item">7</div>
  <div class="flex-item">8</div>

  <!-- A lot of elements !! -->
  <span></span>
  <span></span>
  <span></span>
  <span></span>
  <span></span>
  <span></span>
  <span></span>
</div>

I used display: contents to be able to set N empty elements that will later be considered as 2*N which can reduce the code.

If you will have 7 columns, we will only need 6 extra elements. We can use the two pseudo elements then only 2 empty element to cover the remaining 4.

Peter Mortensen
  • 30,030
  • 21
  • 100
  • 124
Temani Afif
  • 211,628
  • 17
  • 234
  • 311
  • This is great, I am still surprised that there isn't a built in solution to this. – Ben Allen Mar 29 '19 at 14:16
  • @BenAllen we will probably have this in a future iteration ;) CSS grid and Flexbox are still new and they will get updated more and more. – Temani Afif Mar 29 '19 at 14:22
4

You can't explicitly do it either for a flexbox or for a CSS grid - but you can use a hack using CSS variables (usually that's all you need).


CSS Grid Layout

For instance, you can set the global column number in the :root while a specific column number on the grid wrapper - see a CSS grid below with four columns set globally:

:root {
  --columns: 3;
}

.grid {
  display: grid;
  grid-gap: 10px;
  /* Adjusting for the 10px grid-gap as well */
  grid-template-columns: repeat(auto-fit, minmax(calc(100% / var(--columns) - 20px), 1fr));
}

.grid > * {
  background-color: green;
  height: 200px;
}
<div class="grid">
  <div>1</div>
  <div>2</div>
  <div>3</div>
  <div>4</div>
  <div>5</div>
  <div>6</div>
</div>

Now you can set a rule for the number of columns on the grid container by redeclaring --columns (or you can have JavaScript add a class that contains the --columns declaration) - see the demo below:

:root {
  --columns: 3;
}

.grid {
  display: grid;
  grid-gap: 10px;
  --columns: 4; /* Redefine the number of columns */
  grid-template-columns: repeat(auto-fit, minmax(calc(100% / var(--columns) - 20px), 1fr));
}

.grid > * {
  background-color: green;
  height: 200px;
}
<div class="grid">
  <div>1</div>
  <div>2</div>
  <div>3</div>
  <div>4</div>
  <div>5</div>
  <div>6</div>
</div>

Flexbox

Similar arguments are valid for flexboxes - see a simplified demo below:

:root {
  --columns: 3;
}

.flexbox {
  display: flex;
  flex-wrap: wrap;
  --columns: 4; /* Redefine the number of columns */
}

.flexbox > * {
  background-color: green;
  height: 200px;
  margin: 10px;
  flex-basis: calc(100% / var(--columns) - 20px);
}
<div class="flexbox">
  <div>1</div>
  <div>2</div>
  <div>3</div>
  <div>4</div>
  <div>5</div>
  <div>6</div>
</div>
Peter Mortensen
  • 30,030
  • 21
  • 100
  • 124
kukkuz
  • 39,721
  • 6
  • 52
  • 88
  • How compatible is this at the moment? Because it looks quite useful. – Mihail Minkov Mar 21 '19 at 15:45
  • So in this case what type of property is `--columns`. From what I understand you basically convert it into a number using the `var()` function, but how does it work on the `:root` element? – Mihail Minkov Mar 21 '19 at 15:54
  • 2
    they are called *CSS variables* but they are not in the *strict sense* - just *substitutable* properties that you can use... [`:root`](https://developer.mozilla.org/en-US/docs/Web/CSS/:root) represents the `html` element – kukkuz Mar 21 '19 at 15:57
  • So basically they would be interpreted as rules that are not understandable for the browser until they are converted in a variable using the `var()` function? – Mihail Minkov Mar 21 '19 at 15:58
  • yeah, essentially when you use `var(--columns)`, the *value* of that *variable* is substituted there... – kukkuz Mar 21 '19 at 15:59
  • 1
    Ok, that looks quite useful. :) – Mihail Minkov Mar 21 '19 at 16:00
-1

With a little bit of math, you can add a percentage in a maximum function within the minmax. That way, it will break at 200 pixels, but expand to fill 1/3 of the page (in this case because I used 33%. Just add whatever percentage you want there though).

grid-template-columns: repeat(auto-fit, minmax(max(200px, 33%), 1fr));

Peter Mortensen
  • 30,030
  • 21
  • 100
  • 124