Search code examples
htmlcssflexboxcss-grid

Responsive two to three column layout in css


I'm trying to achieve a particular flex/grid responsive behavior that grows from two to three columns, expanding only two containers. I illustrated the desired results below. I was able to manage the two-column stage. However, I don't know how to expand this to three columns using css flex-box.

I chose flex-box as this got me closest to where I need to get. However, the solution must by no means be limited to flex-box.

enter image description here

Situation:

  • 5 containers in total.
  • 3 have a fixed but individual height, 2 should grow in their hight dimension.
  • The containers are organized in 2 columns for small screens and 3 columns for large screens as shown on the sketch.
  • Between the 2 and 3 column stages and beyond the three column stage, the width of all 5 containers should equal 50% (33% respectively) of the parent element's width.

let wf = 0.5;
let wm = 500;

function simulateWidthUpdate() {
  const g = document.getElementById('grid')
  wf = wf == 1 ? 0.5 : 1;
  g.style.width = parseInt(wf * wm) + 'px';
}
button {
 margin: 10px 0;
}

#grid {
  border: 1px solid red;
  width: 250px;
  height: 400px;
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  gap: 10px;
}

.item {
  border-radius: 10px;
  background: #233C3C;
  color: #fff;
  padding: 10px;
  text-align: center;
  flex-grow: 0;
  max-width: 100px;
  width: 100%;
}

#item-c,
#item-d {
  flex-grow: 1;
  min-height: 200px;
}

#item-a {
   height: 20px;
}


#item-b {
   height: 40px;
}

#item-e {
   height: 50px;
}
<button onclick="simulateWidthUpdate()">Alternate Width</button>
<div id="grid">
  <div class="item" id="item-a">A</div>
  <div class="item" id="item-c">B</div>
  <div class="item" id="item-e">C</div>
  <div class="item" id="item-b">D</div>
  <div class="item" id="item-d">E</div>
</div>


Solution

  • You could consider explicitly organizing the columns as separate elements. Then use JavaScript to organize the elements as per your wireframes when simulateWidthUpdate() runs.

    let wf = 0.5;
    let wm = 500;
    
    const a = document.getElementById('item-a');
    const b = document.getElementById('item-b');
    const c = document.getElementById('item-c');
    const d = document.getElementById('item-d');
    
    function simulateWidthUpdate() {
      const g = document.getElementById('grid')
      wf = wf == 1 ? 0.5 : 1;
      g.style.width = parseInt(wf * wm) + 'px';
      
      if (wf === 1) {
        a.insertAdjacentElement('afterend', b);
        
        const newColumn = document.createElement('div');
        newColumn.className = 'column';
        newColumn.appendChild(c);
        document.querySelector('.column').insertAdjacentElement('afterend', newColumn);
      } else {
        d.insertAdjacentElement('beforebegin', b);
        a.insertAdjacentElement('afterend', c);
        document.querySelectorAll('.column')[1].remove();
      }
    }
    button {
     margin: 10px 0;
    }
    
    #grid {
      border: 1px solid red;
      width: 250px;
      height: 400px;
      display: flex;
      gap: 10px;
    }
    
    .column {
      display: flex;
      flex-direction: column;
      gap: 10px;
      flex-grow: 1;
    }  
    
    .item {
      border-radius: 10px;
      background: #233C3C;
      color: #fff;
      padding: 10px;
      text-align: center;
      flex-grow: 0;
      max-width: 100px;
      width: 100%;
    }
    
    #item-c,
    #item-d {
      flex-grow: 1;
      min-height: 200px;
    }
    
    #item-a {
       height: 20px;
    }
    
    
    #item-b {
       height: 40px;
    }
    
    #item-e {
       height: 50px;
    }
    <button onclick="simulateWidthUpdate()">Alternate Width</button>
    <div id="grid">
      <div class="column">
        <div class="item" id="item-a">A</div>
        <div class="item" id="item-c">C</div>
        <div class="item" id="item-e">E</div>
      </div>
      <div class="column">
        <div class="item" id="item-b">B</div>
        <div class="item" id="item-d">D</div>
      </div>
    </div>

    Pure CSS

    You could use container queries and CSS grid:

    let wf = 0.5;
    let wm = 500;
    
    function simulateWidthUpdate() {
      const g = document.getElementById('grid')
      wf = wf == 1 ? 0.5 : 1;
      g.style.width = parseInt(wf * wm) + 'px';
    }
    button {
     margin: 10px 0;
    }
    
    #grid {
      border: 1px solid red;
      width: 250px;
      height: 400px;
      container-type: inline-size;
    }
    
    .inner {
      display: grid;
      grid-template:
        "a b" 40px
        "c b" 10px
        "c d" minmax(200px, 1fr)
        "e d" 70px;
      gap: 10px;
      width: 100%;
      height: 100%;
    }
    
    @container (width > 400px) {
      .inner {
        grid-template:
          "a c d" max-content
          "b c d" max-content
          "e c d" max-content
          "x c d" 1fr;
      }
    }
    
    .item {
      border-radius: 10px;
      background: #233C3C;
      color: #fff;
      padding: 10px;
      text-align: center;
      flex-grow: 0;
      max-width: 100px;
      width: 100%;
    }
    
    #item-c,
    #item-d {
      min-height: 200px;
    }
    
    #item-a {
       height: 20px;
       grid-area: a;
    }
    
    
    #item-b {
       height: 40px;
       grid-area: b;
    }
    
    #item-c {
       grid-area: c;
    }
    
    #item-d {
       grid-area: d;
    }
    
    #item-e {
       height: 50px;
       grid-area: e;
    }
    <button onclick="simulateWidthUpdate()">Alternate Width</button>
    <div id="grid">
      <div class="inner">
        <div class="item" id="item-a">A</div>
        <div class="item" id="item-b">B</div>
        <div class="item" id="item-c">C</div>
        <div class="item" id="item-d">D</div>
        <div class="item" id="item-e">E</div>
      </div>
    </div>

    You could use a style attribute selector instead of container queries:

    let wf = 0.5;
    let wm = 500;
    
    function simulateWidthUpdate() {
      const g = document.getElementById('grid')
      wf = wf == 1 ? 0.5 : 1;
      g.style.width = parseInt(wf * wm) + 'px';
    }
    button {
     margin: 10px 0;
    }
    
    #grid {
      border: 1px solid red;
      width: 250px;
      height: 400px;
      display: grid;
      grid-template:
        "a b" 40px
        "c b" 10px
        "c d" minmax(200px, 1fr)
        "e d" 70px;
      gap: 10px;
    }
    
    #grid[style*="500px"] {
      grid-template:
        "a c d" max-content
        "b c d" max-content
        "e c d" max-content
        "x c d" 1fr;
    }
    
    .item {
      border-radius: 10px;
      background: #233C3C;
      color: #fff;
      padding: 10px;
      text-align: center;
      flex-grow: 0;
      max-width: 100px;
      width: 100%;
    }
    
    #item-c,
    #item-d {
      min-height: 200px;
    }
    
    #item-a {
       height: 20px;
       grid-area: a;
    }
    
    
    #item-b {
       height: 40px;
       grid-area: b;
    }
    
    #item-c {
       grid-area: c;
    }
    
    #item-d {
       grid-area: d;
    }
    
    #item-e {
       height: 50px;
       grid-area: e;
    }
    <button onclick="simulateWidthUpdate()">Alternate Width</button>
    <div id="grid">
      <div class="item" id="item-a">A</div>
      <div class="item" id="item-b">B</div>
      <div class="item" id="item-c">C</div>
      <div class="item" id="item-d">D</div>
      <div class="item" id="item-e">E</div>
    </div>

    Or a "brute-force" approach similar to the first JavaScript solution, where we duplicate the DOM for both instances and switch between them using a style attribute selector or container query:

    let wf = 0.5;
    let wm = 500;
    
    function simulateWidthUpdate() {
      const g = document.getElementById('grid')
      wf = wf == 1 ? 0.5 : 1;
      g.style.width = parseInt(wf * wm) + 'px';
    }
    button {
     margin: 10px 0;
    }
    
    #grid {
      border: 1px solid red;
      width: 250px;
      height: 400px;
      display: flex;
      gap: 10px;
      container-type: inline-size;
    }
    
    .column {
      display: flex;
      flex-direction: column;
      gap: 10px;
      flex-grow: 1;
    }  
    
    .item {
      border-radius: 10px;
      background: #233C3C;
      color: #fff;
      padding: 10px;
      text-align: center;
      flex-grow: 0;
      max-width: 100px;
      width: 100%;
    }
    
    #item-c,
    #item-d {
      flex-grow: 1;
      min-height: 200px;
    }
    
    #item-a {
       height: 20px;
    }
    
    
    #item-b {
       height: 40px;
    }
    
    #item-e {
       height: 50px;
    }
    
    @container (width >= 500px) {
      .wide-hide {
        display: none;
      }
    }
    
    @container (width < 500px) {
      .wide-show {
        display: none;
      }
    }
    <button onclick="simulateWidthUpdate()">Alternate Width</button>
    <div id="grid">
      <div class="column">
        <div class="item" id="item-a">A</div>
        <div class="item wide-show" id="item-b">B</div>
        <div class="item wide-hide" id="item-c">C</div>
        <div class="item" id="item-e">E</div>
      </div>
      <div class="column wide-show">
        <div class="item" id="item-c">C</div>
      </div>
      <div class="column">
        <div class="item wide-hide" id="item-b">B</div>
        <div class="item" id="item-d">D</div>
      </div>
    </div>