CSS - Scroll inside a parent while hovering a fixed child
Asked Answered
L

2

8

I have an internally-scrollable element. Inside it, there's content that can be scrolled through, along with a position: fixed; child forming something of a sidebar.

If I hover anywhere inside the parent and scroll, the parent's contents scroll just as expected. However, if I hover inside the fixed child, which is still inside the bounding box of the parent, and scroll, it does not:

<style>
  body,
  html {
    margin: 0;
  }
  
  #container {
    position: relative;
    width: 100%;
    height: 100vh;
    overflow-y: auto;
  }
  
  #sidebar {
    position: fixed;
    right: 10px;
    width: 100px;
    top: 10px;
    height: calc(100% - 20px);
    background-color: red;
  }
</style>

<div id='container'>

  <div id='sidebar'>
    <a href='https://google.com/'>Link to click</a>
    When hovering here, scroll does NOT work.
  </div>

  <p>Lorem ipsum.</p>
  <p>Lorem ipsum.</p>
  <p>Lorem ipsum.</p>
  ... and so on. When hovering here, scroll works.

</div>

I understand the use of this functionality, but I am interested in making the scroll event still propagate up from the position:fixed; child to its overflow:auto; parent, allowing the user to scroll the parent element, even while hovering the child. Is this possible?

I've already tried using JavaScript to watch for scroll events on the child, then pass them up to the parent, but no such scroll events are caught (I imagine as the element itself has nothing to scroll internally). I've also tried a variety of configurations of the parent element (position:relative;, position:absolute;, position:fixed;), all to no avail. The issue only occurs when the child is fixed, but setting it as absolute isn't an option.

Preferably, I'd like to accomplish this without JavaScript, so a breakdown of possible solutions (HTML-only, HTML and CSS, and JavaScript-only) might be useful.


I'm aware of this question, but the first answer there does not work, as scrolling while hovering the fixed child does not trigger a scroll event (for either child or parent). The second answer isn't doable either, as it moves the child outside of the parent, which is not an option in my case.


Edit: Harsh Karanpuria suggested I add pointer-events: none; to the sidebar. This does accomplish the desired result, but it also makes elements in the sidebar unclickable, which I'd rather not do! After their answer, I also experimented with making child elements of the sidebar pointer-events: all; again, but found that, while that restored my ability to click them, once again removed the scrollable nature while hovering that grandchild.

Limp answered 3/12, 2019 at 6:33 Comment(2)
Have you ever found a solution for this?Equipollent
@AlexanderLe I'm afraid not. In the end, my solution in 2019 was just to not use position: fixed; on the sidebar at all and instead make it go where I wanted some other way (in my case, with display: inline-block;, though grid and flex would usually be better choices, and float: right; would be acceptable).Limp
S
3

position: fixed; is Not Designed For This; Use position: sticky;

I'm going to take a prescriptive stance here: you don't need to fool around with event propagation or anything like that to achieve the effect you're looking for. That might have been the case in the past, but CSS has better tools for this than position: fixed;.

Taking a look at the CSS3 Positioned Layout Module, here's what they have to say about fixed positioning:

Fixed positioning, […] absolutely positions the box and affixes it to the viewport or page frame so that it is always visible.

This is at odds with your interpretation, emphasis mine:

However, if I hover inside the fixed child, which is still inside the bounding box of the parent, […]

Visually, yes, the fixed child is within its parent… but just adding a margin to the parent will move it out of the way:

<style>
  body,
  html {
    margin: 0;
  }
  
  #container {
    /* <Changes> */
    background-color: skyblue;
    margin-top: 10vh;
    /* </Changes> */
    position: relative;
    width: 100%;
    height: 100vh;
    overflow-y: auto;
  }
  
  #sidebar {
    position: fixed;
    right: 10px;
    width: 100px;
    top: 10px;
    height: calc(100% - 20px);
    background-color: red;
  }
</style>

<div id='container'>

  <div id='sidebar'>
    <a href='https://google.com/'>Link to click</a>
    When hovering here, scroll does NOT work.
  </div>

  <p>Lorem ipsum.</p>
  <p>Lorem ipsum.</p>
  <p>Lorem ipsum.</p>
  ... and so on. When hovering here, scroll works.

</div>

I couldn't find the reason that events on position:fixed children don't propagate to the parent as usual in the W3C specs, but the case remains that scroll events propagate to the viewport, instead of bubbling to the parent element as usual.

From the W3C CSS Positioned Layout Module Level 3 § 2:

[position: fixed:] Same as absolute, except the box is positioned and sized relative to a fixed positioning containing block (usually the viewport in continuous media, or the page area in paged media). The box’s position is fixed with respect to this reference rectangle: when attached to the viewport it does not move when the document is scrolled, and when attached to the page area is replicated on every page when the document is paginated. This positioning scheme is called fixed positioning and is considered a subset of absolute positioning.

position: fixed elements are meant to be fixed to the viewport, not to their parent element. The natural solution to your problem is to use position:sticky.

Using position:sticky and display: flex

<style>
  body,
  html {
    margin: 0;
  }
  
  #container {
    /* <Changes> */
    background-color: skyblue;
    display: flex;
    max-height: 40vh; /* Just to force scrolling in the demo. */
    /* </Changes> */
    position: relative;
    width: 100%;
    height: 100vh;
    overflow-y: auto;
  }
  
  #sidebar {
    position: sticky;
    right: 0px;
    top: 0px;
    min-width: 100px;
    background-color: red;
  }
</style>

<div id='container'>
  <!-- Changes -->
  <div id="content">
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    ... and so on. When hovering here, scroll works. Note this really long line is not overlapped by the sidebar.
  </div>
  <!-- /Changes -->
  <div id='sidebar'>
    <a href='https://google.com/'>Link to click</a>
    When hovering here, scroll ALSO works.
  </div>
</div>

I've changed the HTML slightly, putting the <p> tags in a containing div#content and rearranging the order that the content and sidebar go in. You could change flex-direction and forgo the rearranging. The thrust is the same: container element, inner content, inner sidebar.

The effect of this layout differs from the one presented in the question: The sidebar cannot overlap the content in this layout. If that's not desirable, you can do a little bit better by using display:grid.

Using position:sticky and display: grid

We can make a grid area just for the sidebar, like so:

<style>
  body,
  html {
    margin: 0;
  }
  
  #container {
    /* <Changes> */
    background-color: skyblue;
    display: grid;
    grid-template-columns: 1fr min-content;
    
    max-height: 40vh; /* Just to force scrolling in the demo. */
    /* </Changes> */
    position: relative;
    width: 100%;
    height: 100vh;
    overflow-y: auto;
  }
  
  #content {
    grid-row: 1 / span 2;
    grid-column: 1 / span 2;
  }
  
  #sidebar {
    grid-row: 1;
    grid-column: 2;
    position: sticky;
    right: 0px;
    top: 0px;
    min-width: 100px;
    min-height: 110px;
    background-color: red;
  }
  
</style>

<div id='container'>
  <!-- Changes -->
  <div id="content">
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    ... and so on. When hovering here, scroll works. Note this really long line is overlapped by the sidebar.
  </div>
  <!-- /Changes -->
  <div id='sidebar'>
    <a href='https://google.com/'>Link to click</a>
    When hovering here, scroll ALSO works.
  </div>
</div>

Note that to get the overlap to work, we need to manually set grid-column and grid-row on the #sidebar and #content elements.

See also:

Sena answered 27/7, 2022 at 18:46 Comment(0)
F
4

You can achieve this by simply adding:

#sidebar {
    ...
    pointer-events: none;
    ...
}
Funch answered 3/12, 2019 at 7:32 Comment(1)
That's an excellent point, thank you! Unfortunately that also makes the contents of the sidebar unclickable. Is there any way to only bubble up scroll? Sorry, I should have specified that as a requirement! :)Limp
S
3

position: fixed; is Not Designed For This; Use position: sticky;

I'm going to take a prescriptive stance here: you don't need to fool around with event propagation or anything like that to achieve the effect you're looking for. That might have been the case in the past, but CSS has better tools for this than position: fixed;.

Taking a look at the CSS3 Positioned Layout Module, here's what they have to say about fixed positioning:

Fixed positioning, […] absolutely positions the box and affixes it to the viewport or page frame so that it is always visible.

This is at odds with your interpretation, emphasis mine:

However, if I hover inside the fixed child, which is still inside the bounding box of the parent, […]

Visually, yes, the fixed child is within its parent… but just adding a margin to the parent will move it out of the way:

<style>
  body,
  html {
    margin: 0;
  }
  
  #container {
    /* <Changes> */
    background-color: skyblue;
    margin-top: 10vh;
    /* </Changes> */
    position: relative;
    width: 100%;
    height: 100vh;
    overflow-y: auto;
  }
  
  #sidebar {
    position: fixed;
    right: 10px;
    width: 100px;
    top: 10px;
    height: calc(100% - 20px);
    background-color: red;
  }
</style>

<div id='container'>

  <div id='sidebar'>
    <a href='https://google.com/'>Link to click</a>
    When hovering here, scroll does NOT work.
  </div>

  <p>Lorem ipsum.</p>
  <p>Lorem ipsum.</p>
  <p>Lorem ipsum.</p>
  ... and so on. When hovering here, scroll works.

</div>

I couldn't find the reason that events on position:fixed children don't propagate to the parent as usual in the W3C specs, but the case remains that scroll events propagate to the viewport, instead of bubbling to the parent element as usual.

From the W3C CSS Positioned Layout Module Level 3 § 2:

[position: fixed:] Same as absolute, except the box is positioned and sized relative to a fixed positioning containing block (usually the viewport in continuous media, or the page area in paged media). The box’s position is fixed with respect to this reference rectangle: when attached to the viewport it does not move when the document is scrolled, and when attached to the page area is replicated on every page when the document is paginated. This positioning scheme is called fixed positioning and is considered a subset of absolute positioning.

position: fixed elements are meant to be fixed to the viewport, not to their parent element. The natural solution to your problem is to use position:sticky.

Using position:sticky and display: flex

<style>
  body,
  html {
    margin: 0;
  }
  
  #container {
    /* <Changes> */
    background-color: skyblue;
    display: flex;
    max-height: 40vh; /* Just to force scrolling in the demo. */
    /* </Changes> */
    position: relative;
    width: 100%;
    height: 100vh;
    overflow-y: auto;
  }
  
  #sidebar {
    position: sticky;
    right: 0px;
    top: 0px;
    min-width: 100px;
    background-color: red;
  }
</style>

<div id='container'>
  <!-- Changes -->
  <div id="content">
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    ... and so on. When hovering here, scroll works. Note this really long line is not overlapped by the sidebar.
  </div>
  <!-- /Changes -->
  <div id='sidebar'>
    <a href='https://google.com/'>Link to click</a>
    When hovering here, scroll ALSO works.
  </div>
</div>

I've changed the HTML slightly, putting the <p> tags in a containing div#content and rearranging the order that the content and sidebar go in. You could change flex-direction and forgo the rearranging. The thrust is the same: container element, inner content, inner sidebar.

The effect of this layout differs from the one presented in the question: The sidebar cannot overlap the content in this layout. If that's not desirable, you can do a little bit better by using display:grid.

Using position:sticky and display: grid

We can make a grid area just for the sidebar, like so:

<style>
  body,
  html {
    margin: 0;
  }
  
  #container {
    /* <Changes> */
    background-color: skyblue;
    display: grid;
    grid-template-columns: 1fr min-content;
    
    max-height: 40vh; /* Just to force scrolling in the demo. */
    /* </Changes> */
    position: relative;
    width: 100%;
    height: 100vh;
    overflow-y: auto;
  }
  
  #content {
    grid-row: 1 / span 2;
    grid-column: 1 / span 2;
  }
  
  #sidebar {
    grid-row: 1;
    grid-column: 2;
    position: sticky;
    right: 0px;
    top: 0px;
    min-width: 100px;
    min-height: 110px;
    background-color: red;
  }
  
</style>

<div id='container'>
  <!-- Changes -->
  <div id="content">
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    <p>Lorem ipsum.</p>
    ... and so on. When hovering here, scroll works. Note this really long line is overlapped by the sidebar.
  </div>
  <!-- /Changes -->
  <div id='sidebar'>
    <a href='https://google.com/'>Link to click</a>
    When hovering here, scroll ALSO works.
  </div>
</div>

Note that to get the overlap to work, we need to manually set grid-column and grid-row on the #sidebar and #content elements.

See also:

Sena answered 27/7, 2022 at 18:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.