防止页面滚动但允许叠加层滚动

596

我一直在寻找一种“灯箱”类型的解决方案,可以实现此功能,但目前还没有找到(如果您知道任何信息,请提供建议)。

我试图重新创建的行为就像您在Pinterest上点击图像时看到的那样。该覆盖层是可滚动的(就像页面上一页上移一样整个覆盖层向上移动),但是覆盖层后面的主体是固定的。

我尝试只使用CSS来创建它(例如,在整个页面和带有overflow:hidden的body上放置一个

覆盖层
),但无法防止
可滚动。

如何保持主体/页面不滚动但在全屏容器内滚动?


这不就是一个像其他数百个插件一样使用固定位置和纵向滚动溢出的“覆盖”插件吗? - ggzone
4
Pinterest的链接不太有用,因为其内容被登录墙限制了。 - 2540625
9
2017年更新:即使您登录Pinterest,您会发现OP所描述的覆盖效果已不再存在 - 相反,当您点击一张图片时,您只是导航到显示该图片大版本的普通页面。 - Annabel
一个相当不错的CodePen:https://codepen.io/anon/pen/oEMmrm - undefined
23个回答

777

理论

查看当前Pinterest网站的实现(它可能会在将来更改),当您打开叠加层时,noscroll类被应用于body元素(设置overflow: hidden),使得body不再可滚动。

在页面中创建或已注入并通过display: block显示的叠加层 - 没有区别 - 具有position: fixedoverflow-y: scroll,并将topleftrightbottom属性设置为0:这种样式使得叠加层填充整个视口(但现在是2022年,因此您可以使用inset: 0代替)。

叠加层内部的div处于position: static状态,因此垂直滚动条与该元素相关。这导致了一个可滚动但固定的叠加层。

当您关闭叠加层时,必须隐藏它(使用display: none),甚至可以通过javascript删除节点(或仅删除其中的内容,这取决于内容的性质)。

最后一步是还原应用于bodynoscroll类(使overflow属性恢复到先前的值)


代码

Codepen示例

(它通过更改叠加层的aria-hidden属性来显示和隐藏它,并增加其可访问性)。

标记
(打开按钮)

<button type="button" class="open-overlay">OPEN LAYER</button>

(覆盖层和关闭按钮)

<section class="overlay" aria-hidden="true" tabindex="-1">
  <div>
    <h2>Hello, I'm the overlayer</h2>
    ...   
    <button type="button" class="close-overlay">CLOSE LAYER</button>
  </div>
</section>

CSS

.noscroll { 
  overflow: hidden;
}

.overlay { 
   position: fixed; 
   overflow-y: scroll;
   inset: 0; }

[aria-hidden="true"]  { display: none; }
[aria-hidden="false"] { display: block; }

Javascript (vanilla-JS)

var body = document.body,
    overlay = document.querySelector('.overlay'),
    overlayBtts = document.querySelectorAll('button[class$="overlay"]'),
    openingBtt;
    
[].forEach.call(overlayBtts, function(btt) {

  btt.addEventListener('click', function() { 
     
     /* Detect the button class name */
     var overlayOpen = this.className === 'open-overlay';
     
     /* storing a reference to the opening button */
     if (overlayOpen) {
        openingBtt = this;
     }
     
     /* Toggle the aria-hidden state on the overlay and the 
        no-scroll class on the body */
     overlay.setAttribute('aria-hidden', !overlayOpen);
     body.classList.toggle('noscroll', overlayOpen);
     
     /* On some mobile browser when the overlay was previously
        opened and scrolled, if you open it again it doesn't 
        reset its scrollTop property */
     overlay.scrollTop = 0;
     
      /* forcing focus for Assistive technologies but note:
    - if your modal has just a phrase and a button move the
      focus on the button
    - if your modal has a long text inside (e.g. a privacy
      policy) move the focus on the first heading inside 
      the modal
    - otherwise just focus the modal.

    When you close the overlay restore the focus on the 
    button that opened the modal.
    */
    if (overlayOpen) {
       overlay.focus();
    }
    else {
       openingBtt.focus();
       openingBtt = null;
    }

  }, false);

});

/* detect Escape key when the overlay is open */
document.body.addEventListener('keyup', (ev) => {
   if (ev.key === "Escape" && overlay.getAttribute('aria-hidden') === 'false') {
      overlay.setAttribute('aria-hidden', 'true');
      body.classList.toggle('noscroll', false);
      openingBtt.focus();
      openingBtt = null;
   }
})

最后,这里有另一个示例,其中覆盖层通过应用于opacity属性的CSS transition实现淡入效果。此外,当滚动条消失时,还应用了padding-right以避免底部文本的重新布局。

Codepen示例(淡入)

CSS

.noscroll { overflow: hidden; }

@media (min-device-width: 1025px) {
    /* not strictly necessary, just an experiment for 
       this specific example and couldn't be necessary 
       at all on some browser */
    .noscroll { 
        padding-right: 15px;
    }
}

.overlay { 
     position: fixed; 
     overflow-y: scroll;
     inset: 0;
}

[aria-hidden="true"] {    
    transition: opacity 1s, z-index 0s 1s;
    width: 100vw;
    z-index: -1; 
    opacity: 0;  
}

[aria-hidden="false"] {  
    transition: opacity 1s;
    width: 100%;
    z-index: 1;  
    opacity: 1; 
}

9
这是正确的答案。有人应该给它一个勾选标记,因为它是正确的。 - Scoota P
71
这是绝对正确的,但请注意,在移动Safari中它不起作用。背景将继续滚动,而您的覆盖层将会固定。 - a10s
21
它运行良好。可滚动的视窗容器div必须有CSS样式position:fixed,并具有垂直溢出滚动。我成功使用了overflow-y:auto;,而对于iOS的惯性滚动,我在CSS中添加了-webkit-overflow-scrolling:touch;。我使用了display:block;width:100%;height:100%; CSS来实现全屏视窗。 - Slink
11
抱歉,我不明白。将body的属性设置为overflow: hidden并不能禁用我的iPad iOS7上的弹性滚动。因此,我不得不在我的js中添加以下代码:document.body.addEventListener('touchmove', function (event) { event.preventDefault(); }, false);很遗憾,这样也会禁用所有其他可滚动的元素。到目前为止,我还没有找到任何解决方案(不使用额外的插件)。 - Garavani
31
如果用户在激活覆盖层时已经将页面向下滚动到底部,当您设置overflow:hidden时,这会导致页面跳回顶部。有什么解决方法吗? - Björn Andersson
显示剩余17条评论

106

6
我特意前来告诉你,这在最新版的Chrome、Mozilla和Opera上运行正常。祝你过得愉快! - Elated Coder
4
应该有人在这上面加个赏金。这是正确的解决方案,不需要 JavaScript。 :) - joseph.l.hunsaker
45
这个解决方案存在的问题是,它只在覆盖层可以滚动时才有效。如果你有一个弹出框并且它完全适合屏幕,没有滚动条——它将无法停止页面主体的滚动。此外,它在Safari中根本不起作用 :) - waterplea
1
我采用了这个方法与 overflow-y: scroll 的组合(来自你的 Codepen 演示)。@pokrishka 提出的观点是正确的,但在我的情况下不是问题。无论如何,我想知道浏览器的实现是否会在未来涵盖这个细节。我发现,在 Firefox 中,调整浏览器大小以使模态框不适合屏幕,然后再次调整大小以使其适合屏幕可以使此属性起作用(即滚动被包含),即使调整为全屏大小 - 至少在重新加载页面之前是这样的。 - Marc.2377
现在在Safari触摸和桌面上似乎运行正常。 - undefined
显示剩余2条评论

88

如果您想在iOS上防止页面过度滚动,可以将position fixed添加到您的.noscroll类中。

body.noscroll{
    position:fixed;
    overflow:hidden;
}

130
由于该解决方案的固定位置,页面会自动滚动到内容顶部,可能会对用户造成干扰。 - tzi
9
使用position:fixed 的另一个问题是它会调整我的主体大小。也许与其他CSS冲突了,但只需要使用overflow:hidden即可解决。 - Dex
3
当您通过输入框使用 Tab 键时,会出现焦点跳转的问题。 - Atav32
@am80l 抱歉,我的先前留言不够清楚:选项卡顺序没问题,但在 iOS Safari 上通过字段进行选项卡时会使屏幕抖动(https://dev59.com/pIzda4cB1Zd3GeqPnH2L) - Atav32
2
哈哈哈。我打开了 CodePen,然后出现了一个 CodePen 的覆盖层要求注册账户。我无法滚动背景。这太巧了。 - JBis
显示剩余3条评论

61

大多数解决方案存在一个问题,即它们无法保留滚动位置,因此我研究了Facebook是如何做的。除了将底层内容设置为position: fixed之外,他们还动态设置顶部以保留滚动位置:

scrollPosition = window.pageYOffset;
mainEl.style.top = -scrollPosition + 'px';

然后,当你再次移除遮罩层时,你需要重新设置滚动位置:

window.scrollTo(0, scrollPosition);

我创建了一个小例子来演示这个解决方案。

let overlayShown = false;
let scrollPosition = 0;

document.querySelector('.toggle').addEventListener('click', function() {
  if (!overlayShown) {
        showOverlay();
  } else {
    removeOverlay();
  }
  overlayShown = !overlayShown;
});

function showOverlay() {
    scrollPosition = window.pageYOffset;
    const mainEl = document.querySelector('.main-content');
    mainEl.style.top = -scrollPosition + 'px';
    document.body.classList.add('show-overlay');
}

function removeOverlay() {
        document.body.classList.remove('show-overlay');
    window.scrollTo(0, scrollPosition);
    const mainEl = document.querySelector('.main-content');
    mainEl.style.top = 0;
}
.main-content {
  background-image: repeating-linear-gradient( lime, blue 103px);
  width: 100%;
  height: 200vh;
}

.show-overlay .main-content {
  position: fixed;
  left: 0;
  right: 0;
  overflow-y: scroll; /* render disabled scroll bar to keep the same width */
/* Suggestion to put: overflow-y: hidden; 
Disabled scrolling still makes a mess with its width. Hiding it does the trick. */
}

.overlay {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.3);
  overflow: auto;
}

.show-overlay .overlay {
  display: block;
}

.overlay-content {
  margin: 50px;
  background-image: repeating-linear-gradient( grey, grey 20px, black 20px, black 40px);
  height: 120vh;
}

.toggle {
  position: fixed;
  top: 5px;
  left: 15px;
  padding: 10px;
  background: red;
}

/* reset CSS */
body {
  margin: 0;
}
<main class="main-content"></main>

  <div class="overlay">
    <div class="overlay-content"></div>
  </div>
  
  <button class="toggle">Overlay</button>


1
这对我来说似乎是最优雅的解决方案。我知道谷歌也经常使用滚动页面和移动元素的技巧。 - forallepsilon
3
这是唯一一个对我百分之百有效的解决方案,我希望我早点找到这个答案。 - user0103
唯一一个对我百分之百有效的答案。干得好,伙计!谢谢。 - Operator

55

不要在 body 上使用 overflow: hidden;。它会自动将所有内容滚动到顶部。也不需要使用 JavaScript。可以使用 overflow: auto;。这种解决方案甚至适用于移动版 Safari。

HTML 结构

<div class="overlay">
    <div class="overlay-content"></div>
</div>

<div class="background-content">
    lengthy content here
</div>

样式

.overlay{
    position: fixed;
    top: 0px;
    left: 0px;
    right: 0px;
    bottom: 0px;
    background-color: rgba(0, 0, 0, 0.8);

    .overlay-content {
        height: 100%;
        overflow: scroll;
    }
}

.background-content{
    height: 100%;
    overflow: auto;
}

点击此处查看演示,并在此处获取源代码。

更新:

对于想要使用键盘空格、上下翻页的人:您需要将焦点放在覆盖层上,例如,单击它或手动JS聚焦于其上,在这部分div响应键盘之前。当覆盖层“关闭”时也是如此,因为它只是将覆盖层移动到一边。否则,对于浏览器来说,这些仅仅是两个普通的div元素,它不会知道为什么应该关注其中一个。


5
不错的解决方案,但这意味着我需要将所有内容包装在一个div中,而我并没有这样做的打算... - Jacob Raccuia
3
这是一个理想的解决方案。如果有人遇到问题,可能需要添加 html,body {height:100%;}(如演示中所示)才能正常工作。 - John
5
@user18490,angular/material 部分与此解决方案的运作无关。 - Lucia
12
这会在许多方面破坏移动设备的用户体验(例如:URL地址栏隐藏、超出滚动效果等),甚至在桌面上也会影响到使用空格键进行滚动。 - nitely
2
这是更加健壮的选项,但它会使事情变得有点复杂。例如,在刷新时PageUp和PageDown无法使用。任何使用.offset()值进行计算的内容都会出现问题。 - BlackPanther
显示剩余10条评论

36
值得注意的是,有时将“overflow:hidden”添加到body标签中并不能达到效果。在这种情况下,您还需要将该属性添加到html标签中。
html, body {
    overflow: hidden;
}

对于iOS,您还需要设置 width: 100%; height: 100%; - Gavin
5
许多其他解决方案也存在一个问题,即当模态框打开时,如果窗口滚动到页面顶部以外的任何位置,它将导致页面滚动到页面顶部的问题。我发现Philipp Mitterer在这里提供的解决方案是涵盖大多数情况的最佳选项。 - CLL
1
我从未遇到过你所说的这个问题(无论是在网页还是移动端)。你能否分享一下完整的代码(包括DOM),并放在fiddle或jsbin上,以便我们查看哪里出了问题?无论如何,我不太愿意使用JavaScript解决这个问题。 - hodgef
它会破坏整个页面中的任何“sticky”定位。请小心!同时,由于某些情况下(特别是swiper或flex样式),宽度/高度所需大小未知,因此也会破坏它们。 - Patryk

20

您想要防止的行为称为滚动链接。要禁用它,请设置

overscroll-behavior: contain;

在CSS中对您的覆盖图层进行样式处理。


1
这个答案对我有用,但并未被选为正确答案。 - Kranchi

9

如果有人在寻找一个React函数组件的解决方案,你可以将以下内容放在模态框组件中:

 useEffect(() => {
    document.body.style.overflowY = 'hidden';
    return () =>{
      document.body.style.overflowY = 'auto';
    }
  }, [])

这在iOS设备上不起作用。 - Alfrex92

9
你可以使用一些“新”的 CSS 和 JQuery 轻松实现这一点。
最初:`body {... overflow:auto;}` 通过使用 JQuery,您可以动态地在“覆盖”和“正文”之间进行切换。当在“正文”上时,使用
body {
   position: static;
   overflow: auto;
}

当使用“覆盖层”时

body {
   position: sticky;
   overflow: hidden;
}

针对切换('body'到'overlay')的JQuery:

$("body").css({"position": "sticky", "overflow": "hidden"});

将JQuery用于开关('overlay' -> 'body'):

$("body").css({"position": "static", "overflow": "auto"});

在我的特殊情况下,“sticky”是必需的,其他技术会隐藏页面元素/使页面跳转滚动位置稍微有些。 - Willster

7

通常来说,如果你想要一个父元素(在这个例子中是body)在一个子元素(在这个例子中是覆盖层overlay)滚动时阻止它自己滚动,那么就需要把子元素变成父元素的兄弟元素,以防止滚动事件冒泡到父元素。如果父元素是body,那么需要添加一个额外的包装元素:

<div id="content">
</div>
<div id="overlay">
</div>

请查看 使用浏览器主滚动条滚动特定 DIV 内容 以查看其工作方式。


1
最佳解决方案,真正的“出奇制胜”的思维方式,只是措辞不太恰当。 - Mark
1
如果页面主体不滚动,则在手机端向下滚动页面时,顶部栏不会向上滑动。 - Seph Reed

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接