DaveAlden
DaveAlden

Reputation: 30356

Responsively collapse 2 column layout to single column

I currently have a page which is comprised of boxes of content, laid out such that there are two floated columns of inequal width, and each box with a varying height that fits its content, something like this:

.box {
  padding: 1em;
  border: 1px solid black;
  margin: 3px;
}

.left.col {
  float: left;
  width: 70%;
}

.right.col {
  float: right;
  width: 30%;
}

.b1 {
  background-color: red;
  height: 150px;
}

.b2 {
  background-color: green;
  height: 80px;
}

.b3 {
  background-color: blue;
  height: 80px;
}

.b4 {
  background-color: yellow;
  height: 40px;
}

.b5 {
  background-color: purple;
  height: 30px;
}

.b6 {
  background-color: cyan;
  height: 40px;
}

.b7 {
  background-color: orange;
  height: 40px;
}
<div class="boxes">
  <div class="left col">
    <div class="box b1">1</div>
    <div class="box b2">2</div>
    <div class="box b3">3</div>
  </div>
  <div class="right col">
    <div class="box b4">4</div>
    <div class="box b5">5</div>
    <div class="box b6">6</div>
    <div class="box b7">7</div>
  </div>
</div>

I'd like to be able to responsively collapse that layout on smaller screens into a single column but in such a way that I am able to control the ordering of the content boxes. For that I'm happy to use the flexbox order with an appropriate polyfill for unsupported browsers such as flexibility. Something like this:

/* @media screen and (max-width: 570px) */
.boxes {
  display: flex;
  flex-direction: column;
}

.box {
  padding: 1em;
  border: 1px solid black;
  margin: 3px;
}

.b1 {
  background-color: red;
  height: 150px;
  order: 1
}

.b2 {
  background-color: green;
  height: 80px;
  order: 3
}

.b3 {
  background-color: blue;
  height: 80px;
  order: 5
}

.b4 {
  background-color: yellow;
  height: 40px;
  order: 2;
}

.b5 {
  background-color: purple;
  height: 30px;
  order: 4
}

.b6 {
  background-color: cyan;
  height: 40px;
  order: 6
}

.b7 {
  background-color: orange;
  height: 40px;
  order: 7
}
<div class="boxes">
  <div class="box b1">1</div>
  <div class="box b2">2</div>
  <div class="box b3">3</div>
  <div class="box b4">4</div>
  <div class="box b5">5</div>
  <div class="box b6">6</div>
  <div class="box b7">7</div>
</div>

What I haven't managed to do so far is achieve this as a single, responsive approach. I think I'd need to get rid of the <div class="col"> in order to control the box order with flex, but can't seem to achieve the same 2 column layout with flexbox and without float.

Is there a single (CSS) solution where both layouts can be achieved and switched responsively?

Edit

Both IE9 and Android 4.x account for around 2% each of my current audience of the past year, so I still need to support them. Therefore any solution using modern CSS techniques (e.g. Flexbox, CSS grid) needs to be backed up with a polyfill or gracefully fallback/degrade.

Upvotes: 2

Views: 4558

Answers (3)

Asons
Asons

Reputation: 87191

Since the only pure CSS solution is CSS Grid, and given the fact that you need to support i.a. IE9, you will also need a script/fallback to accomplish this.

As Flexbox can't do this well either, dynamically (will need either fixed height or script), the absolute simplest and easiest to maintain solution, is to keep the wrappers, and just move the elements back and forth with a simple script.

Combined with that the script adds a class to the body, we can use CSS to control the elements.

Note 1; To make this more dynamic, the script can be optimized and make use of e.g. a set attribute on the element, where it should be positioned, though I didn't went that far here.

Note 2; The script toggle the layout based on orientation, though can be updated to use a width.

Note 3; The resize event make use of a throttler, which I initially found at MDN, to avoid expensive operations such as DOM modifications.

Stack snippet

(function(d, timeout) {

  function resizing() {
    if (window.innerHeight < window.innerWidth) {
      if (!(d.body.classList.contains('landscape'))) {
        d.body.classList.add('landscape');
        var target = d.querySelector('.right.col');
        target.appendChild(d.querySelector('.b4'));
        target.appendChild(d.querySelector('.b5'));
        target.appendChild(d.querySelector('.b6'));
        target.appendChild(d.querySelector('.b7'));
      }
    } else {
      if (d.body.classList.contains('landscape')) {
        d.body.classList.remove('landscape')
        var target = d.querySelector('.left.col');
        target.insertBefore(d.querySelector('.b4'), d.querySelector('.b2'))
        target.insertBefore(d.querySelector('.b5'), d.querySelector('.b3'))
        target.appendChild(d.querySelector('.b6'));
        target.appendChild(d.querySelector('.b7'));
      }
    }
  }

  /* event handlers */
  window.addEventListener("load", function() {
    resizing();
  }, false);

  window.addEventListener("resize", function() {
    if (!timeout) {
      timeout = setTimeout(function() {
        timeout = null;
        resizing();
      }, 66);
    }
  }, false);

}(document));
.box {
  padding: 1em;
  border: 1px solid black;
  margin: 3px;
}

.left.col {
  float: left;
  width: 100%;
}
.landscape .left.col {          /*  added rule  */
  width: 70%;
}

.right.col {
  float: right;
  width: 30%;
}

.b1 {
  background-color: red;
  height: 150px;
}

.b2 {
  background-color: green;
  height: 80px;
}

.b3 {
  background-color: blue;
  height: 80px;
}

.b4 {
  background-color: yellow;
  height: 40px;
}

.b5 {
  background-color: purple;
  height: 30px;
}

.b6 {
  background-color: cyan;
  height: 40px;
}

.b7 {
  background-color: orange;
  height: 40px;
}
<div class="boxes">
  <div class="left col">
    <div class="box b1">1</div>
    <div class="box b2">2</div>
    <div class="box b3">3</div>
  </div>
  <div class="right col">
    <div class="box b4">4</div>
    <div class="box b5">5</div>
    <div class="box b6">6</div>
    <div class="box b7">7</div>
  </div>
</div>

Upvotes: 1

Michael Benjamin
Michael Benjamin

Reputation: 371221

If you can only use flexbox, then the layout is simple using nested containers or a height or max-height on the container.

The most efficient CSS solution for this layout would use Grid technology.

The solution below is complete and browser support is quite strong.

jsFiddle demo

.boxes {
  display: grid;
  grid-template-columns: 3fr 1fr;
  grid-auto-rows: 10px;
  grid-gap: 5px;
  grid-template-areas:
   " red yellow"
   " red yellow"
   " red yellow"
   " red yellow"
   " red yellow"
   " red yellow"   
   " red purple"
   " red purple"
   " red purple"
   " red purple"
   " red purple"   
   " red cyan"
   " red cyan"
   " red cyan"
   " red cyan"
   " green cyan"
   " green orange "
   " green orange "
   " green orange "
   " green orange "
   " green orange "   
   " green . " 
   " green . " 
   " green . " 
   " blue . " 
   " blue . "  
   " blue . "    
   " blue . "  
   " blue . "  
   " blue . "  
   " blue . "   
   " blue . "     
}

.b1 { grid-area: red;    background-color: red;    }
.b2 { grid-area: green;  background-color: green;  }
.b3 { grid-area: blue;   background-color: blue;   }
.b4 { grid-area: yellow; background-color: yellow; }
.b5 { grid-area: purple; background-color: purple; }
.b6 { grid-area: cyan;   background-color: cyan;   }
.b7 { grid-area: orange; background-color: orange; }

.box {
  padding: 1em;
  border: 1px solid black;
}

@media (max-width: 570px) {
  .boxes { grid-template-columns: 1fr;
           grid-template-areas: 
   " red "
   " red "
   " red "
   " red "
   " red "
   " red "
   " red "
   " red "
   " red "
   " red "
   " red "   
   " red "
   " red "
   " red "
   " red "
   " yellow "
   " yellow "
   " yellow "
   " yellow "
   " yellow "
   " yellow "
   " green "
   " green "
   " green "
   " green "
   " green "
   " green "   
   " green " 
   " green " 
   " green " 
   " purple "
   " purple "
   " purple "
   " purple "
   " purple "
   " blue " 
   " blue "  
   " blue "    
   " blue "  
   " blue "  
   " blue "  
   " blue "   
   " blue "     
   " cyan "
   " cyan "
   " cyan "
   " cyan "
   " cyan "
   " orange "
   " orange "
   " orange "
   " orange "
   " orange " ;}

}
<div class="boxes">
  <div class="box b1">1</div>
  <div class="box b2">2</div>
  <div class="box b3">3</div>
  <div class="box b4">4</div>
  <div class="box b5">5</div>
  <div class="box b6">6</div>
  <div class="box b7">7</div>
</div>

Features:

  • modern CSS3 technology
  • a container with two columns (3fr and 1fr) (more about the fr unit)
  • rows are automatically created, as needed; each row is 10px tall...
  • so a grid area that spans four rows is 40px (plus any grid-row-gap)
  • grid areas are laid out using string values in the grid-template-areas property
  • grid areas could be laid out using other methods (such as line-based placement)

Upvotes: 3

Ronnie Smith
Ronnie Smith

Reputation: 18565

Nesting flexboxes seems to work but I'm not sure about debugging responsive mode in this SO embedded HTML engine. Seem like something below should do it though. Basically 3 flexboxes where the outer flexbox toggles between row and column / 100% width...

.boxes{
display:flex;

justify-content:center;
align-content:center;
align-items:center;
}

.col{
display:flex;
flex-flow: column nowrap;
justify-content:center;
align-content:center;
align-items:center;
}

.left{
flex: 0 1 auto;
min-width: 70%;
}
.right{
flex: 0 1 auto;
align-self:flex-start;
min-width: 30%;
}

.box{
flex: 0 1 auto;
min-width:100%
}


.b1{ background-color: red; height: 150px;}
.b2{ background-color: green; height: 80px;}
.b3{ background-color: blue; height: 80px;}
.b4{ background-color: yellow; height: 40px;}
.b5{ background-color: purple; height: 30px;}
.b6{ background-color: cyan; height: 40px;}
.b7{ background-color: orange; height: 40px;}

@media screen (max-width: 639px) {
.boxes{
flex-flow: column nowrap;
}
.left{
min-width: 100%;
}
.right{
min-width: 100%;
}
}
@media screen (min-width: 640px) {
.boxes{
flex-flow: row nowrap;
}
}
   
<div class="boxes">
<div class="left col">
    <div class="box b1">1</div>
    <div class="box b2">2</div>
    <div class="box b3">3</div>
</div>
<div class="right col">
    <div class="box b4">4</div>
    <div class="box b5">5</div>
    <div class="box b6">6</div>
    <div class="box b7">7</div>
</div>
</div>

Upvotes: 1

Related Questions