Search code examples
cssflexboxcss-grid

Why is the flex-wrap of my container causing my auto-fill grid to add extra empty tracks row?


Taking 3 items of 100px height in a grid with grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));, and putting that grid into a flex container with flex-wrap: wrap; will push the flex container to have a height of 332px, instead of the 100px height of the grid (tested chrome and safari).

I just banged my head on this problem before finding the solution : set flex-wrap: nowrap;. I do realized it also have sometimes to do about flex-direction: columns;, but I don't understand why this flex properties has effects on a grid. Can someone explains? Also open to cleaner fixes.

Codepen

.flex-container {
  background-color: #aaa;
  display: flex;
  flex-direction: column;
  width: 100%;
  flex-wrap: wrap;
  /*problem is here, solved with "nowrap"*/
}

.grid-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 16px;
  padding-inline-start: 0px;
  margin: 0;
}

.item {
  background-color: #cc0;
  height: 100px
}
<span>flex-container should have a height of 100px, but has 332px instead because of flex-wrap set to "wrap"</span>
<div class="flex-container">
  <ul class="grid-container">
    <li class="item"></li>
    <li class="item"></li>
    <li class="item"></li>
  </ul>
</div>


Solution

  • It's related to the default stretch alignment of flexbox. If you change the alignment of the grid container you can see what is happening:

    .flex-container {
      background-color: #aaa;
      display: flex;
      flex-direction: column;
      width: 100%;
      flex-wrap: wrap;
      /*problem is here, solved with "nowrap"*/
    }
    
    .grid-container {
      align-self: start;
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
      gap: 16px;
      padding-inline-start: 0px;
      margin: 0;
    }
    
    .item {
      background-color: #cc0;
      height: 100px
    }
    <span>flex-container should have a height of 100px, but has 332px instead because of flex-wrap set to "wrap"</span>
    <div class="flex-container">
      <ul class="grid-container">
        <li class="item"></li>
        <li class="item"></li>
        <li class="item"></li>
      </ul>
    </div>

    The grid is no more full width and the result is logic. Even if you use nowrap you will have the same result.


    The trick is that your grid container is a flex item that has no explicit width definition and is using an auto-fill configuration. This configuration means that you grid need to first get a width defined by it's environment then based on that it will define the columns. In other words, the content play no role in defining the width of the element.

    In your case, the stretch alignment is defining the width making the element take all the space available. Removing the stretch alignment will remove the only width definition and your grid will get the minimum size possible (the size of one column).

    Now the difference between wrap and nowrap is how the algorithm works. I will try to use easy words to explain it.

    When using nowrap, you have a single line configuration

    A single-line flex container (i.e. one with flex-wrap: nowrap) lays out all of its children in a single line, even if that would cause its contents to overflow. ref

    This configuration is somehow easy because the flex container will first create one line taking all the available space. Then inside that line the elements will get placed (in your case the grid container is stretched) and we can easily find the final height of the line and the container.

    When using wrap, you have a multi-line configuration even if in the end you have only one line

    A multi-line flex container (i.e. one with flex-wrap: wrap or flex-wrap: wrap-reverse) breaks its flex items across multiple lines,

    This will make the algorithm tricky because when we have a multi-line configuration we have to also identify the size of the lines (in your case it's the width since you have a column configuration) and we said that the grid container doesn't have any explicit width so it cannot size the line but the line will get size with the stretch algorithm as well. Not the same stretch of the grid container!

    flex lines obey to the align-content property and by default is has the value stretch and flex items obey to align-items (or align-self) property which also has a stretch default alignment. So we have two nested stretch alignment.

    In the below I will update the align-content and we get the same behavior as my first snippet.

    .flex-container {
      background-color: #aaa;
      display: flex;
      flex-direction: column;
      width: 100%;
      flex-wrap: wrap;
      align-content: start;
      /*problem is here, solved with "nowrap"*/
    }
    
    .grid-container {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
      gap: 16px;
      padding-inline-start: 0px;
      margin: 0;
    }
    
    .item {
      background-color: #cc0;
      height: 100px
    }
    <span>flex-container should have a height of 100px, but has 332px instead because of flex-wrap set to "wrap"</span>
    <div class="flex-container">
      <ul class="grid-container">
        <li class="item"></li>
        <li class="item"></li>
        <li class="item"></li>
      </ul>
    </div>

    This configuration makes the algorithm a bit complex because we first need to find the height of the lines without the stretch alignment (the 332px you get) then we apply the stretch alignment to the lines increasing their cross size (their width). The increase of this width will also increase the width of the grid since this one is also stretched inside the line and here comes the trick! The grid container have more space to create more columns BUT we don't get back to calculate the height again because it was already done in a previous step hence we keep that 332px of height.

    It may sounds strange but the double stretch alignment combined with auto-fill created a complex and very specific situation where we first compute the height that we don't update later to avoid getting into an infinite loop.


    To avoid such situation, you either keep a single line configuration by using nowrap or you explicitly define the width of your grid container (width: 100% will do the job if you won't have more items in your flex container) or you simply get rid of flex-direction: column which is not really need in your case. But you have to add flex-grow: 1 to the grid container.

    The stretch alignment works on the cross axis and when using a row configuration it will be the height and flex item will have a shrink-to-fit behavior on the main axis but you can make them grow to fill the available space

    .flex-container {
      background-color: #aaa;
      display: flex;
    }
    
    .grid-container {
      display: grid;
      flex-grow: 1;
      grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
      gap: 16px;
      padding-inline-start: 0px;
      margin: 0;
    }
    
    .item {
      background-color: #cc0;
      height: 100px
    }
    <span>flex-container should have a height of 100px, but has 332px instead because of flex-wrap set to "wrap"</span>
    <div class="flex-container">
      <ul class="grid-container">
        <li class="item"></li>
        <li class="item"></li>
        <li class="item"></li>
      </ul>
    </div>

    The above configuration have less complexity and is more logic for your situation and it won't create any "strange" result


    Here is another related question with a similar situation if you want more headaches: flex-wrap causes children to double in height