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 verticalflex
s 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.