2

The layout I am trying to achieve is like this, exactly filling the entire viewport of the browser:

[----fixed height top "menu bar"----]
/-----\/----------------------------\
|fixed||                            |
|width|| both ways stretchy content |
|side ||                            |
|bar  ||                            |
\-----/\----------------------------/

I have it nearly working, but I haven't figured out how to specify that the horizontally-arranged boxes vertically fill the space available to them rather than the height of the page. As it currently stands, whenever the content in the sidebar is long enough to cause it to gain a scroll bar, I end up with an additional vertical scrollbar on the entire page which scrolls by exactly the height of the top bar, which is not what I want.

The following 1998-styled demo page illustrates the problem. It actually produces the layout I want in Safari 9, but not Chrome or Firefox.

Note these additional constraints, which are already satisfied by the demo and must be preserved:

  • There may be an arbitrary number of fixed-width or stretchy boxes arranged in the horizontal direction.

  • Each side bar or main content has an overflow-y: auto vertical scrollbar. This must also work (that is, long content must not make the overall flexbox layout exceed the viewport size). Some of the sidebars themselves use flexbox layout in the vertical direction.

  • A 90%-10% layout is not suitable for the purpose. Nor is allowing some of the content to be clipped. The entire reason I have this layout problem is that the application should be using every pixel of the screen to show useful information or controls, so any extra whitespace or clipped content is undesirable.

<!doctype html>
<html style="height: 100%;">
<title>Flexbox test</title>
<style type="text/css">
#parent-of-topbar {
  height: 100%;  box-sizing: border-box; margin: 0;
  background: #FCC;

  display: flex; flex-direction: column;
}
#topbar {
  padding: .2em;
  background: white;
  border: solid black;
  border-width: 0 0 .3em 0;
  color: black;
  flex: 0 0 auto;
}
#main {
  display: flex; flex-direction: row;
  
  flex: 1 1 auto;
  max-height: 100%; 
}
.subwindow {
  overflow-x: clip;
  overflow-y: auto;
  max-height: 100%;

  border: 3px outset #CCC;
  background: linear-gradient(to right, #CCC 0%,#AAA 100%);
}
.fixed {
  flex: 0 0 auto;
  width: 10em;
}
.stretchy {
  flex: 1 1 auto;
  width: 100%;
}
</style>

<body id="parent-of-topbar">
  <div id="topbar">top bar content</div>
  <div id="main">
    <div class="subwindow stretchy">stretchy part</div>
    <div class="subwindow fixed">
      fixed sidebar 1; this should be scrollable, not stretch the content
      <details open><div style="font-size: 3em;">spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam</div></details>
    </div>
    <div class="subwindow fixed">
      2nd fixed sidebar
    </div>
  </div>
</body>

Here is a screenshot of the layout I want to achieve, faked by hardcoding a dimension:

Here is a screenshot of the layout the current demo gets on Chrome:

enter image description here

Michael Benjamin
  • 346,931
  • 104
  • 581
  • 701
Kevin Reid
  • 37,492
  • 13
  • 80
  • 108
  • Your question is a bit unclear. The first paragraph seems to raise three different issues. – Michael Benjamin Oct 25 '15 at 04:02
  • @Michael_B Edited — I've separated out the “this is what I already have, which must not be broken by the proposed solution” parts. How's that? – Kevin Reid Oct 25 '15 at 04:08
  • Yes, thanks. But only Safari 9 renders the code above properly, correct? Using Chrome or FF on my PC won't work? – Michael Benjamin Oct 25 '15 at 04:11
  • Well, “properly” in the sense that it gets the result that I want. I don't know which one is implementing the spec properly. – Kevin Reid Oct 25 '15 at 04:16
  • One option you have is to change the `max-height: 100%` on `#main` and `.subwindow` to `height: calc(100% - 18px)`. 18px being the computed height of the fixed top bar. – Michael Benjamin Oct 25 '15 at 04:25
  • Also, since you're emphasizing the need to keep everything within the viewport (using vertical scrollbars when necessary), you may want to consider using [**viewport percentage units**](https://css-tricks.com/viewport-sized-typography/) for `height`, as opposed to straight percentages. – Michael Benjamin Oct 25 '15 at 04:33
  • @KevinReid There are no fixed heights yet this part confuses me: > fixed sidebar 1; this should be scrollable, not stretch the content. < When the `
    ` is collapsed, it's siblings (the other 2 boxes) shrink as well. Is that behavior desirable?
    – zer00ne Oct 25 '15 at 04:35
  • @Michael_B I didn't know about those, so thanks for the tip in general, but I don't think they're applicable here (this is a “cram as much _data_ onto the screen as fits” application). – Kevin Reid Oct 25 '15 at 04:40
  • @zer00ne Aha, I missed something in my description. The way it looks when collapsed is actually preferable. But when the `
    ` is expanded, there should be just the one scrollbar for the sidebar, not the one on the entire page.
    – Kevin Reid Oct 25 '15 at 04:43
  • @KevinReid If cramming is your motive, then Michael_B is right, I was just finishing my answer with `height: 100vh` and `width: 100vw` – zer00ne Oct 25 '15 at 04:43
  • @zer00ne Nothing should _scale_ with the viewport (within the scope of this question, anyway). Every dimension is either fixed (rather: `em` units) or fill-remaining-space. – Kevin Reid Oct 25 '15 at 04:45
  • @KevinReid I'm sorry I do not grok. > that is, long content must not make the overall flexbox layout exceed the viewport size – zer00ne Oct 25 '15 at 04:50
  • I have added screenshots, including a “faked” one which shows the goal. – Kevin Reid Oct 25 '15 at 04:58
  • Check the update in Full Page – zer00ne Oct 25 '15 at 05:00

3 Answers3

3

UPDATE

After a bit of back and forth in the comments we came up with the solution. It's a simple fix, but wasn't simple to discover.

The answer boils down to this:

  • In the original code, the horizontal boxes are wrapped in a container having flex: 1 1 auto.

  • For the layout to work the container needs to have flex: 1 1 0 (or flex: 1, for short).

  • The original code caused a vertical scrollbar on the viewport window because flex: 1 1 auto sizes the item based on content size or height properties.

  • The adjusted code results in a layout that fits neatly in the viewport because flex: 1 1 0 sizes the item based on the free space in the flex container.

DEMO

The flexbox spec goes into more detail in section 7.1.1. Common Values of flex.


I have it nearly working, but I haven't figured out how to specify that the horizontally-arranged boxes vertically fill the space available to them rather than the height of the page. As it currently stands, whenever the content in the sidebar is long enough to cause it to gain a scroll bar, I end up with an additional vertical scrollbar on the entire page which scrolls by exactly the height of the top bar, which is not what I want.

Each side bar or main content has an overflow-y: auto vertical scrollbar [which must be preserved].

You have two flex items: #topbar and #main.

The height of #topbar is based on content, padding and border (it has no specified height).

The height of #main is max-height: 100% (which, on Chrome and FF, computes to height: 100%).

When you add these two heights together the sum exceeds the height: 100% applied to their container (#parent-of-topbar). That's the reason for the vertical scrollbar on the body.

Solution #1

The simplest way to resolve this issue, while preserving the vertical scrollbar on the content boxes, is to use overflow: hidden.

Add this to the CSS:

body { height: 100%; overflow: hidden; }

DEMO: http://jsfiddle.net/mwfkLu8b/

This is a blunt force object method. It gets the job done quickly, easily and effectively, but it costs you the lower portion of the screen. In other words, overflow: hidden clips the area where the vertical scrollbar was going.

Solution #2

Another method to resolve the issue, which also preserves the vertical scrollbar on content boxes, but doesn't clip any content, is to distribute the 100% height among flex items.

Try this:

* { box-sizing: border-box; } /* new */

body { height: 100%; } /* new */

#topbar {
    padding: .2em;
    background: white;
    border: solid black;
    border-width: 0 0 .3em 0;
    color: black;
    flex: 0 0 auto;
    height: 10%; /* new */
}

#main {
    display: flex;
    flex-direction: row;
    flex: 1 1 auto;
    /* max-height: 100%; */
    height: 90%; /* new */ 
}

DEMO: http://jsfiddle.net/mwfkLu8b/1/

If #topbar needs to be a fixed height, then you can use pixels and calc to equal 100%.

#topbar { height: 40px; }
#main { height: calc(100% - 40px); }

DEMO: http://jsfiddle.net/mwfkLu8b/2/

Michael Benjamin
  • 346,931
  • 104
  • 581
  • 701
  • Distributing the vertical height is what I want, but it _should_ be distributed in the way flex grow/shrink does, not by using a set of heights that adds up to 100%. But if I can't find a better way, I'll certainly use the `calc` approach. – Kevin Reid Oct 25 '15 at 14:20
  • (Your Solution #1 is inadequate for the application — it could cut off data or additional “sidebar” content within the `.subwindow`s. This needs to be an _exact_ layout.) – Kevin Reid Oct 25 '15 at 14:30
  • You can certainly remove the `height` properties altogether from the flex items and use `flex-basis` instead: http://jsfiddle.net/mwfkLu8b/3/ – Michael Benjamin Oct 25 '15 at 14:35
  • That seems to be functionally equivalent to the version with `height`, and still doesn't allow the top bar to have its natural size; is there some advantage? – Kevin Reid Oct 25 '15 at 14:39
  • You can also remove `height` and `flex-basis` and instead use space distribution (`flex-grow`) to determine size: http://jsfiddle.net/mwfkLu8b/4/ – Michael Benjamin Oct 25 '15 at 14:42
  • That looks like a "10%-90%" style layout. The top bar needs to be _its natural height, or less preferably a fixed height_. – Kevin Reid Oct 25 '15 at 14:57
  • Then remove the `flex-grow` from the top bar, and apply `flex-grow: 1` to `#main`, which tells it consume all available free space: http://jsfiddle.net/mwfkLu8b/5/ – Michael Benjamin Oct 25 '15 at 15:08
  • 1
    Aha! The key component of that which makes it work is specifying a flex-basis of _zero_ (not `auto`) for the stretchy content, which is implicitly done by `flex: 1;`. I had to dig into the details of what your answer was doing to get this. – Kevin Reid Oct 25 '15 at 15:16
  • I've got my actual application working; thanks for the pointers. Would you mind editing your answer to use the code in your http://jsfiddle.net/mwfkLu8b/5/ and explaining the significance of the flex-basis? – Kevin Reid Oct 25 '15 at 15:43
  • 1
    Kevin I may need a day or two to revise this answer, but I'll get to it. I'm glad you got the issue resolved. – Michael Benjamin Oct 25 '15 at 21:29
  • Hi Kevin, answer revised. Sorry it took so long, but I wanted to take some time to study the issue. Finally found the time this morning. – Michael Benjamin Nov 15 '15 at 14:26
  • Yeah, no problem. I wasn't looking for a checkmark. Just wanted to learn the issue and complete the answer. Thanks. – Michael Benjamin Nov 15 '15 at 16:15
1

Thanks to Michael_B's answer and comments, I've figured out the solution which works for the demo as well as my actual application.

The key rule is: do not use height: 100%; or width: 100%; anywhere inside of flexbox. Use flex-grow to make things expand to fit their containers, even if this means converting the content of the .subwindow elements to flexbox layout where it wasn't before.

Furthermore, do not set flex-basis: auto (directly or via the flex shortcut) on stretchy things. Instead use flex-basis: 0% (or any zero length), which is implicitly done by flex: <number>;, and let the growing take care of filling the container. I'm still not sure whether this is necessary if you follow the first rule absolutely, but it seems to help in imperfect cases.

Revised demo (now showing a 100%-like main panel as my real application had, which was another complication):

<!doctype html>
<html style="height: 100%;">
<title>Flexbox test</title>
<style type="text/css">
#parent-of-topbar {
  height: 100%;  box-sizing: border-box; margin: 0;
  background: #FCC;

  display: flex; flex-direction: column;
}
#topbar {
  padding: .2em;
  background: white;
  border: solid black;
  border-width: 0 0 .3em 0;
  color: black;
}
#main {
  display: flex; flex-direction: row;
  
  flex: 1;
}
.subwindow {
  overflow-x: clip;
  overflow-y: auto;

  border: 3px outset #CCC;
  background: linear-gradient(to right, #CCC 0%,#AAA 100%);
  
  display: flex;
  flex-direction: column;
}
.fixed {
  flex: 0 auto;
  width: 10em;
}
.stretchy {
  flex: 1;
}
</style>

<body id="parent-of-topbar">
  <div id="topbar">top bar content</div>
  <div id="main">
    <div class="subwindow stretchy">
      <div style="background: #CFC; flex: 1; vertical-align: middle; text-align: center;">stretchy content</div>
    </div>
    <div class="subwindow fixed">
      fixed sidebar 1; this should be scrollable, not stretch the content
      <details open><div style="font-size: 3em;">spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam</div></details>
    </div>
    <div class="subwindow fixed">
      2nd fixed sidebar
    </div>
  </div>
</body>
Community
  • 1
  • 1
Kevin Reid
  • 37,492
  • 13
  • 80
  • 108
0

Not totally clear on what you want exactly, so let me know so I can accommodate. You mentioned content being in viewport. so I used vh and vw.

html {
  box-sizing: border-box;
  font: 400 16px/1.5 small-caps"Trebuchet MS";
}
*,
*:before,
*:after {
  box-sizing: inherit;
  margin: 0;
  padding: 0;
  border: 0 solid transparent;
  outline: 0;
  text-indent: 0;
}
body {
  background: #FCC;
  color: #000;
  position: relative;
  display: flex;
  flex-direction: column;
  height: 95vh;
  width: 100vw;
}
#topbar {
  padding: .2em;
  background: white;
  border: solid black;
  border-width: 0 0 2px 0;
  color: black;
  line-height: 1.4;
  max-height: 32px;
  flex: 2 0 32px;
}
#main {
  display: flex;
  flex-direction: row;
  flex: 1 0 auto;
  max-height: 100%;
}
.subwindow {
  overflow-x: clip;
  overflow-y: auto;
  max-height: 110%;
  outline: 3px outset #CCC;
  background: linear-gradient(to right, #CCC 0%, #AAA 100%);
}
.fixed {
  width: 10em;
}
.stretch {
  flex: 1 0 10em;
  width: 100%;
}
details > div {
  font-size: 3em;
}
<!doctype html>
<html>

<head>
  <meta charset="utf-8">
  <title>Flexbox test</title>

</head>

<body>
  <header id="topbar">Header</header>
  <main id="main">
    <section class="subwindow stretch">
      <h3>Section 1</h3>
      <h4>Expanding Section</h4>
    </section>
    <section class="subwindow fixed">
      <h3>Section 2</h3> 
      <h4>Scrolling Section</h4>
      <details open>
        <div>spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam spam</div>
      </details>
    </section>
    <section class="subwindow fixed">
      <h3>Section 3</h3>
      <h4>Static Section</h4>
    </section>
  </main>
</body>
zer00ne
  • 41,936
  • 6
  • 41
  • 68
  • When I view this in Chrome, as long as the sidebar content is taller than the viewport, I get page scrollbars in both directions. There should be neither; the only scrollbar that appears should be within the "sidebar 1" element. – Kevin Reid Oct 25 '15 at 04:50
  • Updated header `4vh` and html `95vh` – zer00ne Oct 25 '15 at 04:55
  • The top bar needs to be fixed (`em`) height, not a percentage of the viewport. Think of it as being a row of buttons/links/status displays, not a page heading — it should be exactly as big as it needs to be to be read/clicked and not any bigger. – Kevin Reid Oct 25 '15 at 05:00
  • `#topbar` didn't have any height originally AFAIK. I changed it to a `
    ` for semantics, it's just a fancy `
    `. The computed style originally is 19px and it was 32px with my font settings (`line-height: 2`). So what would you normally set the topbar at? 2em or 3em?
    – zer00ne Oct 25 '15 at 05:03
  • Yes, the default behavior of being exactly as high as its single-line-of-text content is what is wanted. If it were to wrap it would be taller. – Kevin Reid Oct 25 '15 at 05:05
  • @KevinReid Ok no scrollbars take a look – zer00ne Oct 25 '15 at 05:50
  • There's a variable-size stripe of unfilled area at the bottom (presumably due to the `max-height: 98%`. That is unwanted — we are looking for _exactly_ filling the viewport. – Kevin Reid Oct 25 '15 at 14:25
  • Ok no scrollbars no pink area, and it's still has `overflow-y: auto`, but it looks as if you've found another solution, so I'll move on... It looks as if we came to simular ends. – zer00ne Oct 25 '15 at 18:24