javascripthtmlcssflexboxcss-float

Display list in several columns and keep the number of elements per column equal when their heights are different


I simply need to have an unordered list displayed in 3 columns, but without vertical alignment.

Is there a way to remove the gap between the list element no. 2 and no. 5 in this example?

I already tried flexbox, float, table, column and grid layout and can't figure it out, although it looks simple at first glance.

A JS solution would also be OK, but i can't alter the HTML output. And i don't know the item's heights, so i don't need a solution with fixed pixel values.

If there is a solution for this, the following sorting would also be a nice gimmick, but i think i may figure that one out after i get a starting point:

1 3 5
2 4

ul { 
  list-style-type: none; 
  padding: 0;
  color: white; 
  text-align:center;
}

.small { 
  height: 30px
}

.large {
  height: 200px
}

li {
  background:red;
}

li:nth-child(2n) { 
  background:blue;
}

ul {
  display: grid; 
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
<ul>
  <li class="large">1</li>
  <li class="small">2</li>
  <li class="small">3</li>
  <li class="small">4</li>
  <li class="small">5</li>
</ul>

Desired result: enter image description here

Note: This is NOT a question about a masonry layout with only CSS. In the linked answer, the item no. 4 (in the first column) would float into the second row, as a masonry layout is trying to have equal heights for the resulting columns. The question here is about to have the same amount of items in every column (until there are no items left). I already tried the solution over there and it didn't work unsurprisingly.


Solution

  • Use Javascript to split the list items into three new lists. Then add the three new lists as children of the original list.

    This is our starting point: a single list

    <ul id="ul1">
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
      <li>6</li>
      <li>7</li>
      <li>8</li>
      <li>9</li>
      <li>10</li>
    </ul>
    

    This is the goal: three lists

    <ul id="ul1">
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
      </ul>
      <ul>
        <li>5</li>
        <li>6</li>
        <li>7</li>
      </ul>
      <ul>
        <li>8</li>
        <li>9</li>
        <li>10</li>
      </ul>
    </ul>
    

    Here is a working example, which, as an added bonus, determines a random height for each item.

    const getRandomIntInclusive = (min, max) => {
      const minCeiled = Math.ceil(min)
      const maxFloored = Math.floor(max)
      return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled)
    }
    
    const ul1 = document.getElementById('ul1')
    const items = Array.from(ul1.children)
    const columns = []
    const itemColumnIndex = []
    
    /* how many items per column to make all columns the same length, and how many items left over? */
    const itemsPerColumn = Math.floor(items.length / 3)
    const remainder = items.length % 3
    
    // construct three lists, one for each column
    for (let i = 0; i < 3; i++) {
      columns.push(document.createElement('ul'))
      // how many items should be in this column?
      const itemsThisColumn = itemsPerColumn + (i < remainder ? 1 : 0)
      // add n items to the array containing the index of this column
      for (let j = 0; j < itemsThisColumn; j++) {
        itemColumnIndex.push(i)
      }
    }
    
    // process all items
    for (let i = 0; i < items.length; i++) {
      // assign a random height
      items[i].style.height = getRandomIntInclusive(30,100) + 'px'
      // move the item into the appropriate list
      columns[itemColumnIndex[i]].appendChild(items[i])
    }
    
    // add the three lists as children of the original list 
    columns.forEach(c => {
      ul1.appendChild(c)
    })
    ul {
      list-style-type: none; 
      margin: 0;
      padding: 0;
      color: white; 
    }
    
    #ul1 { 
      display: grid; 
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 1em;
      
      li {
        display: flex;
        justify-content: center;
        align-items: center;
        background:red;
      }
    
      li:nth-child(2n) { 
        background:blue;
      }
    }
    <ul id="ul1">
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
      <li>6</li>
      <li>7</li>
      <li>8</li>
      <li>9</li>
      <li>10</li>
    </ul>