8

I'm working on a React app that has a modal with some Tabbed content inside that occasionally needs to scroll. However, the problem is that I can't manage to get the proper content to scroll.

The modal is broken into 3 main sections:

  1. Header
    • Should always be visible at the top of the modal
  2. Content
    • Should fill space between Header and Footer but never force either from view
  3. Footer
    • Should always be visible underneath the Content
    • This may either mean at the bottom of the modal (if Content fill remaining space) or underneath the Content (if Content doesn't fill space)

While this would be simple to implement (and I have already done so), the problem arises in the fact that the Content isn't supposed to scroll, but rather something inside it should. In the images below are two example of the intended behaviour, one with long content that should scroll and one without.

I am using a custom Tabbed component that renders something inside a container. This container (white in the images below) is what should scroll if necessary.

The furthest I have come can be found in the following CodePen and example code. I am currently able to scroll the element that contains intended scrollable content, but can't actually get that content to scroll. If you inspect the Pen you can see that it (for some reason) extends past the containing element.

Modal Examples

Code Example

CodePen Example

HTML

<div class="modal">
  <div class="background" />
  <div class="modal_content">
    <div class="header">Random header</div>
    <div class="tabbed_content">
      <div class="tabs">
        <div class="tab active">Tab 1</div>
        <div class="tab">Tab 2</div>
      </div>
      <div class="content">
        <div class="scrollable">
          Tabbed Content
          <br /><br /><br /><br /><br /><br />
          Another Couple Lines
          <br /><br /><br /><br /><br /><br />
          More Tabbed Content
          <br /><br /><br /><br /><br /><br />
          Even More Content!
          <br /><br /><br /><br /><br /><br />
          Why not more yet!
          <br /><br /><br /><br /><br /><br />
          Some Ending Content
          <br /><br /><br /><br /><br /><br />
          Final Content!
        </div>
      </div>
    </div>
    <div class='footer'>
      <button class='button'>
        Done
      </button>
    </div>
  </div>
</div>

CSS

body {
  font-family: 'Montserrat', sans-serif;
}

/* Modal wrapper */
.modal {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

/* Modal background styles */
.background {
  position: absolute;
  height: 100%;
  width: 100%;
  background-color: rgba(0, 0, 0, 0.5)
}

/* Modal content */
.modal_content {
  height: 100vh;
  display: flex;
  flex-direction: column;
  width: 85%;
  margin: 0 auto;
  background-color: white;
}

.header {
  margin-bottom: 1rem;
  padding: 1rem;
  font-size: 125%;
  text-align: center;
  font-weight: bold;
  background-color: #EEEEEE;
  border-bottom: 1px solid #CECECE;
}

/* Contains the tabs and content */
.tabbed_content {
  display: flex;
  flex-direction: column;
  align-items: stretch;
}

/* IGNORE */
.tabs {
  display: flex;
  /* NOTE: Added to stop them from hiding */
  flex-shrink: 0;
}

/* IGNORE */
.tab {
  flex-grow: 1;
  margin-top: 8px;
  padding: 0.5rem;
  text-align: center;
  background-color: #CECECE;
}

/* IGNORE */
.tab.active {
  margin-top: 0;
  font-weight: bold;
  background-color: #EEEEEE;
}

/* Contains the current tab's content */
/* NOTE: This shouldn't scroll */
.content {
  padding: 1rem;
  background-color: #EEEEEE;

  /* NOTE: Currently the closest I can come */
  /*overflow: auto;*/
}

/* IMPORTANT: This should scroll if necessary! */
.scrollable {
  padding: 0.5rem;
  background-color: white;
  /* NOTE: Can't get this to scroll */
  /*overflow: auto;*/
  border: 1px dashed #CECECE;
}

.footer {
  display: flex;
  flex-align: center;
  flex-shrink: 0;
  justify-content: center;
  margin-top: 1rem;
  padding: 1rem;
  border-top: 1px dashed #CECECE;
}

/* IGNORE */
.button {
  padding: 0.75rem 1rem;
  font-size: 90%;
  color: white;
  background-color: lightseagreen;
  border: none;
  border-radius: 2px;
  cursor: pointer;
}

I am nearly at my wits end with this (have spent 2 hours messing around so far). Thanks in advance for any help!

P.S. I also need to worry about stopping the background scroll while the modal is open at some point...

Kendall
  • 1,992
  • 7
  • 28
  • 46

3 Answers3

10

Generally speaking, for overflow: auto to render a vertical scrollbar, you need to define a height on the container.

In order for overflow to have an effect, the block-level container must have either a set height (height or max-height) or white-space set to nowrap.

source: https://developer.mozilla.org/en-US/docs/Web/CSS/overflow

However, this isn't always a practical solution, especially in dynamic environments.

In your layout, the simple solution is to make the container (.content) a flex container. That's enough to make the layout work in Chrome (demo).

However, for the layout to also work in Firefox and Edge, you need to override the default minimum height of flex items, which is min-height: auto. This prevents flex items from shrinking below the size of their content, which eliminates the possibility for an overflow to occur. (full explanation)

Make these adjustments to your code:

.tabbed_content {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  min-height: 0;                  /* NEW, esp for FF & Edge (see link below) */
}

.content {
  padding: 1rem;
  background-color: #EEEEEE;
  overflow: auto;                /* KEEP THIS, for FF & Edge (see link below) */
  display: flex;                 /* NEW */
  flex-direction: column;        /* NEW */
}

.scrollable {
  padding: 0.5rem;
  background-color: white;
  overflow: auto;               /* RESTORE */
  border: 1px dashed #CECECE;
}

revised codepen

More details: Why doesn't flex item shrink past content size?

Michael Benjamin
  • 346,931
  • 104
  • 581
  • 701
  • 1
    Wow, just wow. That certainly seems like something I should have tried! Thanks so much for this solution. This works well with long and short content as I wanted, so thanks again! I'll give it a shot in the actual app and let you know what happens! – Kendall May 27 '17 at 18:04
  • My original answer worked only in Chrome, but not FF and Edge. I just updated my answer for a more complete cross-browser solution. (I haven't tested in Safari.) – Michael Benjamin May 27 '17 at 18:09
  • 1
    Gotcha, that is certainly important for me. The implementation may be difficult since it is spread across 2 React components (one for TabbedContent and the other for Modal) but it should be doable (likely as conditional (default) property to the TabbedContent). – Kendall May 27 '17 at 18:11
  • Also, I think you had mentioned something about `flex-shrink: 0`. Yes, that's important if you want flex items to respect your specified width / height. See the section ***The `flex-shrink` factor*** in this answer for a more complete explanation: https://stackoverflow.com/q/34352140/3597276 – Michael Benjamin May 27 '17 at 18:19
  • Yes, I had before realizing that the Pen I provided had the same issue. My bad (I am familiar with Flex to an extent, but do run into knowledge gaps about what `flex[grow|shrink|basis]` default to without being specified). – Kendall May 27 '17 at 18:20
  • 1
    I was able to solve the issue using your additions - thank you for them! One of the other problems that your answer prompted me to look for was that I was missing `display: flex` on one of the parent containers - once I added it and several companions styles everything just clicked! It will obviously need to be tested cross-browser, but still! Thanks again! – Kendall May 27 '17 at 19:34
0

Layout like this is tricky! I usually try and build simplified versions and work up.

One way you could do it would be with position absolute for the scrollable div.

https://codepen.io/sheriffderek/pen/zwQxLr/ - see this pen for long and short examples

<aside class="window">

    <header>header</header>

    <main>
        <div class="scroll-area">
            <ul>
                <li>short conent</li>
                <li>2</li>
                <li>3</li>
                <li>4</li>
                <li>5</li>
            </ul>
        </div>
    </main>

    <footer>footer</footer>

</aside>

CSS (this is stylus - but it's just syntax - and the concept should be clear)

.window
    border: 10px solid $gray;
    max-width: 600px // arbitrary
    height: 80vh // arbitrary

    display: flex
    flex-direction: column

    header, main, footer
        padding: $pad

    header
        background: $color

    main
        flex-grow: 1
        background: $alternate
        position: relative

        .scroll-area
            position: absolute
            top: 0
            left: 0
            width: 100%
            height: 100%
            overflow: auto
            padding: $pad

    footer
        background: $highlight

For your additional question, you can add/remove class from the body when opening the modal and stop scrolling.

Prevent body scrolling but allow overlay scrolling

sheriffderek
  • 8,848
  • 6
  • 43
  • 70
  • I will look into using `position: absolute;` - I had tried it at one point but that was before I got to this layout. As you mentioned, I did start simple and worked my way up - I'm getting stuck by the addition of the "TabbedContent" component as the middle "high-level" element and having to scroll within a child element. Also, the shorter content one should not extend to take up the remaining height but should rather only take up the necessary height (may also be what's causing me grief). – Kendall May 27 '17 at 16:29
  • There are a few logic issues - and you'll have to work up one by one. The text area can't really just 'know' how high to be without some specific rules - and CSS may not be able to do everything. I'm betting you'll need some JavaScript to check a few things and set a few things. – sheriffderek May 27 '17 at 16:41
  • 1
    I was actually able to solve my issue with @Michael_B solution posted above. I was indeed missing `display: flex` on a single parent container and several other additional styles once that was added. Thanks for the help! – Kendall May 27 '17 at 19:32
0

You have to set a height on .scrollable as well as overflow-y: auto; otherwise, the overflow is visible as there is no height for it to overflow from.

Tom Harris
  • 243
  • 2
  • 7
  • I have tried adding `overflow: auto;` to the `.scrollable` div to no avail, in addition to adding `height: 100%` on **all** parent elements up to the one with a `calc()` height. – Kendall May 27 '17 at 16:25
  • You need to add a fixed height, it won't work if you use `height: 100%`, try `height: 300px` for example and you'll see it working – Tom Harris May 27 '17 at 19:26
  • Consider adding a fixed height that works most of the time for you, and then using js to add the height of the parent. A simple jquery example would be: `$(".scrollable").css("height", $(".content").height()+"px");` – Tom Harris May 27 '17 at 19:28
  • This was the approach taken by another member of the team but I am not comfortable guessing at a fixed height. The advance of Flexbox has brought a lot of once-impossible things into the realm of possibility (did manage to solve with just Flex now). Thanks anyway though! – Kendall May 27 '17 at 19:31