htmlcsslayoutflexbox

Full page with nested flexbox and scrolling


I'm trying to do what I thought was a fairly standard sort of full-screen layout using flexbox, but it's not behaving the way I want. My goals are:

* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
}

.page {
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.topbar {
  background-color: lightgreen;
}

.vcontainer {
  flex-grow: 1;
  display: flex;
  flex-direction: row;
  min-height: 0;
}

.sidebar {
  overflow-y: auto;
  background-color: yellow;
}

.content {
  flex-grow: 1;
  overflow: auto;
  background-color: lightblue;
}

.footer {
  background-color: pink;
}
<div class="page">
  <nav class="topbar">
    top bar
  </nav>
  <div class="vcontainer">
    <nav class="sidebar">
      side<br/>side<br/>side
    </nav>
    <main class="content">
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
      <p>some actual content</p>
    </main>
  </div>
  <footer class="footer">
    footer
  </footer>
</div>

The above works fine for the full doc scrolling variant (although the magic was adding the min-height to vcontainer; it didn't work prior to that) -- the blue area takes the remaining space and is scrollable if the content gets too long.

Without changing the outer styling (or at least not changing it in a way that breaks the above scenario), I want to be able to sometimes put in some content that essentially has an additional dynamic-height topbar and a scrolling subsection that doesn't trigger the main content scrollbar.

* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
}

.page {
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.topbar {
  background-color: lightgreen;
}

.vcontainer {
  flex-grow: 1;
  display: flex;
  flex-direction: row;
  min-height: 0;
}

.sidebar {
  overflow-y: auto;
  background-color: yellow;
}

.content {
  flex-grow: 1;
  overflow: auto;
  background-color: lightblue;
}

.footer {
  background-color: pink;
}

.contentcontainer {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.contentsub {
  margin: 1rem;
  overflow-y: scroll;
  flex-grow: 1;
  background-color: orange;
}
<div class="page">
  <nav class="topbar">
    top bar
  </nav>
  <div class="vcontainer">
    <nav class="sidebar">
      side<br/>side<br/>side
    </nav>
    <main class="content">
      <div class="contentcontainer">
        <div>
          some additional heading content
        </div>
        <div class="contentsub">
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
        </div>
        <div>
          some additional footer content
        </div>
      </div>
    </main>
  </div>
  <footer class="footer">
    footer
  </footer>
</div>

What I'm wanting is for the blue area to not have a scrollbar in this case, but instead for the orange area to be scrollable, surrounded by the extra header/footer and sized to fill the leftover space. Ideally, this should work even if contentsub isn't a direct child of the contentcontainer but there's more wrapping elements and borders etc before getting to the scrollable element. I would prefer a solution that's "pure flexbox", but if there's a better way to do it then I'm all ears. I do want to avoid using javascript, however.


Solution

  • I got this after a bit of tweaking:

    * {
      box-sizing: border-box;
    }
    
    html,
    body {
      margin: 0;
    }
    
    .page {
      width: 100vw;
      height: 100vh;
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }
    
    .topbar {
      background-color: lightgreen;
    }
    
    .vcontainer {
      flex-grow: 1;
      display: flex;
      flex-direction: row;
      min-height: 0;
    }
    
    .sidebar {
      overflow-y: auto;
      background-color: yellow;
    }
    
    .content {
      flex-grow: 1;
      overflow: auto;
      background-color: lightblue;
    }
    
    .footer {
      background-color: pink;
    }
    
    .contentcontainer {
      height: 100%;
    }
    
    .contentsub {
      max-height: max(40%, 2rem);
      margin: 1rem;
      overflow-y: scroll;
      background-color: orange;
    }
    <div class="page">
      <nav class="topbar">
        top bar
      </nav>
      <div class="vcontainer">
        <nav class="sidebar">
          side<br/>side<br/>side
        </nav>
        <main class="content">
          <div class="contentcontainer">
            <div>
              some additional heading content
            </div>
            <div class="contentsub">
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
              <p>some actual content</p>
            </div>
            <div>
              some additional footer content
            </div>
          </div>
        </main>
      </div>
      <footer class="footer">
        footer
      </footer>
    </div>

    Grid approach:

    * {
      box-sizing: border-box;
    }
    
    html,
    body,
    .page {
      height: 100%;
      margin: 0;
    }
    
    .topbar {
      grid-area: header;
      background-color: lightgreen;
    }
    
    .sidebar {
      grid-area: sidebar;
      overflow-y: auto;
      background-color: yellow;
    }
    
    .content {
      grid-area: content;
      display: flex;
      flex-flow: column nowrap;
      overflow: auto;
      background-color: lightblue;
    }
    
    .footer {
      grid-area: footer;
      background-color: pink;
    }
    
    .contentsub {
      flex-grow: 1;
      margin: 1rem;
      overflow-y: scroll;
      background-color: orange;
    }
    
    @media (min-width: 30rem) {
      .page {
        display: grid;
        grid-template-areas: "header header" "sidebar content" "footer footer";
        grid-template-columns: 1fr 9fr;
      }
    }
    <div class="page">
      <header class="topbar">
        top bar
      </header>
      <aside class="sidebar">
        side<br/>side<br/>side
      </aside>
      <main class="content">
        <div>
          some additional heading content
        </div>
        <div class="contentsub">
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
          <p>some actual content</p>
        </div>
        <div>
          some additional footer content
        </div>
      </main>
      <footer class="footer">
        footer
      </footer>
    </div>