Align arbitrary number of elements with different widths to a grid with wrapping

Edit:

  • As @user943702 has pointed out we can make use of max-content property, to remove the extraneous spaces in each column (do not confuse this property with that coming in the explanation though which is a widths value per element basis, and this one is per column basis)
  • For space distribution : there is a handy property called justify-content I've chosen to set it to center, among other values, you can set it to :

    space-between; /* The first item is flush with the start,the last is flush with the end */
    space-around;  /* Items have a half-size space on either end */
    space-evenly;  /* Items have equal space around them */
    stretch;       /* Stretch 'auto'-sized items to fitthe container */
    

Before getting to the script, there are a couple of notes :

  • You can set it to responsively changing using only css by: @media query one for each width and be done with it however the individual elements have an arbitrary width too so I'm gonna use JavaScript

Edit: Here is a script using the CSS media query method notice that the more you try to customize it to different device widths the more you risk to be caught when individual elements width changes unexpectedly.

const theElements = [{  name: "ele1",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }, {    name: 4  }, {    name: 5  }]}, {  name: "ele2",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele3",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele4",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele5",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele6",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele7",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele8",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele9",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele10",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele11",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }, {    name: 4  }, {    name: 5  }]}, {  name: "ele12",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}];

new Vue({
  el: '#ele-grid',
  data: {
    elements: theElements
  }
});
@media (min-width: 1020px) {
	#ele-grid {
		display:grid;
		grid-template-columns:repeat(5, 1fr); 	
        justify-content: center;
	}
}
@media (min-width:400px) and (max-width: 1020px) {
	#ele-grid {
		display:grid;
		grid-template-columns:max-content max-content max-content; 	
	}
}
@media (max-width: 400px) {
	#ele-grid {
		display:grid;
		grid-template-columns:max-content; 	
	}
}
.ele-card {
  margin: 5px 3px;
}
.ele-card .children {
  display: flex;
  flex-wrap: nowrap;
  padding: 5px;
}
.ele-card .child {
  margin: 0 5px;
  width: 30px;
  height: 30px;
  text-align: center;
  line-height: 30px;
  border: 1px solid black;
  background: magenta;
}
.wrapper{
	border: 1px solid black;
  background: cyan;
  display:inline-block;
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.28/vue.min.js"></script>

<div id="ele-grid">
  <div class="ele-card" v-for="ele in elements" :key="ele.name">
  	<div class="wrapper">
	    <div class="element">{{ele.name}}</div>
	    <div class="children">
	      <div class="child" v-for="child in ele.children" :key="child.name">{{child.name}}</div>
	    </div>
	  </div>
	</div>
</div>

  • To get the width as needed there is an excellent -moz-max-content property unfortunately it is not supported yet by the other browsers, so I've appended a child wrapper and make it display:inline-block which have the intended behavior
  • I'm using CSS grid layout and you can use css columns or vertical flexs instead but the elements would be aligned from top to bottom changing the whole layout.

That was for the css, for the JavaScript:

  • In a nutshell this scripts takes a layout with max columns (here 10 you can increase it), and see if it fits without scrolling, if not decrements.
  • In this script, elements are responsive using a the resize event.

const theElements = [{  name: "ele1",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }, {    name: 4  }, {    name: 5  }]}, {  name: "ele2",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele3",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele4",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele5",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele6",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele7",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele8",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele9",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele10",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}, {  name: "ele11",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }, {    name: 4  }, {    name: 5  }]}, {  name: "ele12",  children: [{    name: 1  }, {    name: 2  }, {    name: 3  }]}];

new Vue({
  el: '#ele-grid',
  data: {
    elements: theElements
  }
});
function resizeHandler(){
	colStart=10; 		// max number of columns to start with
	allCards= document.getElementsByClassName('wrapper');
	totalWidth=0;
	maxWidTab=[];
	for (i=colStart;i>0;i--){
		for(j=0;j<i; j++){									//initializing and resetting
			maxWidTab[j]=0;
		}
		for (j=0; j<allCards.length; j++){
			cellWidth=parseInt(getComputedStyle(allCards[j]).width);		//parseInt to remove the tailing px
			maxWidTab[j%i]<cellWidth?maxWidTab[j%i]=cellWidth:'nothing to be done';
		}
		for(j=0;j<i; j++){									//sum to see if fit
			totalWidth+=maxWidTab[j]+2+6		//borders and margins
		}
		if (totalWidth<innerWidth){
			grEl = document.getElementById("ele-grid");
			grEl.style.gridTemplateColumns="repeat("+i+", max-content)";
			/*console.log(i);*/
			break;
		}else{
				totalWidth=0;							//resetting
		}
	}
}
	window.addEventListener("resize",resizeHandler);
	document.addEventListener ("DOMContentLoaded",resizeHandler);
#ele-grid {
	display:grid;
    justify-content: center;
	grid-template-columns:repeat(10, max-content); 	/* starting by 10 columns*/
}
.ele-card {

  margin: 5px 3px;
}
.ele-card .children {
  display: flex;
  flex-wrap: nowrap;
  padding: 5px;
}
.ele-card .child {
  margin: 0 5px;
  width: 30px;
  height: 30px;
  text-align: center;
  line-height: 30px;
  border: 1px solid black;
  background: magenta;
}
.wrapper{
	border: 1px solid black;
  background: cyan;
  display:inline-block;
}

</style>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.28/vue.min.js"></script>

<div id="ele-grid">
  <div class="ele-card" v-for="ele in elements" :key="ele.name">
  	<div class="wrapper">
	    <div class="element">{{ele.name}}</div>
	    <div class="children">
	      <div class="child" v-for="child in ele.children" :key="child.name">{{child.name}}</div>
	    </div>
	  </div>
	</div>
</div>


CSS grid-template-columns does support content-aware value which is max-content. The only question is that how many columns should be there.

I write an algorithm to probe maximum number of column. The implementation involves JS and requires browser to support CSS Grid. Demo can be found here. (I use Pug to create same source structure as yours and styling is also same as yours so that we can focus on JS panel, the implementation).

In demo, changing viewport size will re-flow grid items. You may trigger re-flow at other interesting moments manually by calling flexgrid(container), e.g. loading items asynchronously then re-flow. Changing dimension properties of items is allowed as long as source structure keeps unchanged.

Here's the algorithm

Step1) Set container as grid formatting context, layout all grid items in one row, set each column width to max-content

|---container---|
|aaaaa|bbb|ccc|ddd|eee|fff|ggggg|hhh|iii|

Step2) find first overflow grid line

|---container---|
|aaaaa|bbb|ccc|ddd|eee|fff|ggggg|hhh|iii|
                  ^overflowed

Step3) reduce grid-template-columns to, in our case, 3. Since grid-row default to auto, CSS engine layouts a grid item on next row when it goes beyond last column grid line. I called this "wrapping". In addition, grid items are auto expanded due to grid-template-columns:max-content(e.g. "ddd" is expanded to the length of widest content of first column)

|---container---|
|aaaaa|bbb|ccc|
|ddd  |eee|fff|
|ggggg|hhh|iii|

Since all column grid lines sit "inside" container, we have done. In some cases, a new overflowed grid line is being introduced after "wrapping", we need to repeat step2&3 until all grid lines sit "inside" container, e.g.

#layout in one row
|---container---|
|aaaaa|bbb|ccc|ddd|eee|fff|ggggggg|hhhhh|iii|

#find the first overflowed grid line
|---container---|
|aaaaa|bbb|ccc|ddd|eee|fff|ggggggg|hhhhh|iii|
                  ^overflowed

#reduce `grid-template-columns`
|---container---|
|aaaaa  |bbb  |ccc|
|ddd    |eee  |fff|
|ggggggg|hhhhh|iii|

#find the first overflowed grid line
|---container---|
|aaaaa  |bbb  |ccc|
|ddd    |eee  |fff|
|ggggggg|hhhhh|iii|
                  ^overflowed

#reduce `grid-template-columns`
|---container---|
|aaaaa  |bbb  |
|ccc    |ddd  |
|eee    |fff  |
|ggggggg|hhhhh|
|iii    |

#find the first overflowed grid line
#None, done.