如果您之前听过这个,请阻止我。您打开一个模态框,在其中滚动,关闭它,然后发现页面位置与打开模态框时不同。
这是因为模态框与页面上的任何其他元素一样,都是页面上的元素。它可能会保持在原位(假设这是它的预期行为),但页面其余部分将继续正常运行。
查看 CodePen 上 Geoff Graham 的示例
在模态对话框显示时避免 Safari 可滚动(@geoffgraham)
在 CodePen 上。
有时这不是问题,例如与视口高度完全相同的屏幕。但是,对于其他任何情况,我们都在谈论“滚动之城”。好消息是,我们可以通过一些 CSS(和 JavaScript)技巧来阻止这种情况。
让我们从简单的事情开始
通过将整个 body 的高度设置为视口的高度并隐藏模态框打开时的垂直溢出,我们可以大幅减少打开模态框时页面滚动™ 的问题。
body.modal-open {
height: 100vh;
overflow-y: hidden;
}
这很好,但是如果我们在打开模态框之前已经通过 <body>
元素滚动过,我们会得到一点水平重排。视口宽度会扩展大约 15 个像素,这正好是滚动条的宽度。
查看 CodePen 上 Geoff Graham 的示例
在模态对话框显示时避免 Safari 可滚动(@geoffgraham)
在 CodePen 上。
让我们稍微调整一下 body 的右侧填充以避免这种情况。
body {
height: 100vh;
overflow-y: hidden;
padding-right: 15px; /* Avoid width reflow */
}
请注意,模态框需要比视口高度短才能使其正常工作。否则,body 上的滚动条将是必要的。
很好,那么移动设备呢?
此解决方案在桌面和 Android 移动设备上运行良好。也就是说,iOS 版 Safari 需要更多关注,因为当点击并移动触摸屏时,模态框打开时 body 仍然可以滚动。
我们可以将 body 设置为固定位置作为解决方法。
body {
position: fixed;
}
现在可以了!触摸屏幕时,body 不会响应。但是,这里仍然存在一个“小”问题。假设模态框触发器在页面下方,我们点击将其打开。很好!但是现在我们自动滚动到屏幕顶部,这与我们试图解决的滚动行为一样令人困惑。
查看 CodePen 上 Geoff Graham 的示例
在模态对话框显示时避免 Safari 可滚动(@geoffgraham)
在 CodePen 上。
糟糕!
这就是为什么我们必须转向 JavaScript 的原因
我们可以使用 JavaScript 来避免触摸事件冒泡。我们都知道,当模态框打开时应该有一个背景层。不幸的是,stopPropagation
在 iOS 中的触摸操作中有点尴尬。但 preventDefault
运行良好。这意味着我们必须在模态框中包含的每个 DOM 节点中添加事件监听器,而不仅仅是在背景或模态框层上。好消息是,许多 JavaScript 库可以做到这一点,包括久经考验的 jQuery。
哦,还有一件事:如果我们需要在模态框中滚动怎么办?我们仍然必须触发触摸事件的响应,但当到达模态框的顶部或底部时,我们仍然需要阻止冒泡。似乎非常复杂,所以我们还没有完全摆脱困境。
让我们增强固定 body 方法
这就是我们一直在使用的
body {
position: fixed;
}
如果我们知道滚动位置的顶部并将其添加到我们的 CSS 中,那么 body 将不会滚动回屏幕顶部,因此问题得到解决。我们可以使用 JavaScript 来计算滚动顶部,并将该值添加到 body 样式中。
// When the modal is shown, we want a fixed body
document.body.style.position = 'fixed';
document.body.style.top = `-${window.scrollY}px`;
// When the modal is hidden, we want to remain at the top of the scroll position
document.body.style.position = '';
document.body.style.top = '';
这可以工作,但是模态框关闭后仍然存在一点泄漏。具体来说,似乎当模态框打开并且 body 设置为固定时,页面已经失去了其滚动位置。所以我们必须检索位置。让我们修改我们的 JavaScript 以考虑这一点。
// When the modal is hidden...
const scrollY = document.body.style.top;
document.body.style.position = '';
document.body.style.top = '';
window.scrollTo(0, parseInt(scrollY || '0') * -1);
就这样!模态框打开时 body 不会再滚动,并且在模态框打开和关闭时都会保持滚动位置。万岁!
查看 CodePen 上 Geoff Graham 的示例
在模态对话框显示时避免 Safari 可滚动(@geoffgraham)
在 CodePen 上。
那这个呢
创建一个
position: fixed; overflow: auto;
覆盖层,扩展到窗口边缘,并将模态框放在该覆盖层内。不幸的是,在 iOS Safari 上不起作用,请尝试此页面,https://bootstrap.ac.cn/docs/4.3/components/modal/
没有提到 overscroll-behavior: contain?在我看来,即使支持尚未到位,这应该是未来的解决方案。
不幸的是,
overscroll-behavior: contain
不会在内容小于或等于父级(即没有溢出)时阻止滚动。如果这成为可能(或在浏览器中修复?)您确实可以只在模态框和背景上设置 overscroll 行为,一切都将变得很棒且高效。
您只需要在同一元素上也设置
overflow: auto
,或者执行任何将该元素变成滚动容器的操作。即使是你的“万岁”在我的 Android Chrome 上也会滚动。
请问详细情况如何?
感谢分享这篇文章,Brad!我们在一些网站上使用了这种技术,我一直在想写一篇关于它的文章,但现在你帮我省去了麻烦!真的非常感谢。
奇怪的是,在某些项目中,这似乎与
transform: translateY(...)
配合使用效果更好,但并不一致。有时会导致闪烁,有时速度明显更快。只是想分享一下,以防其他人能从垂直偏移属性的实验中获益。 ♂我知道
transform: translateY(...)
在过渡时会破坏position: fixed
。你是这个意思吗?一个真正的问题:您不能只在
html
元素而不是body
上设置overflow: hidden
从而完全消除此问题吗?(除了滚动条的水平重排之外)我知道它在 Windows 上的 Firefox/Chrome/Edge/IE 中有效,但尚未检查 Safari。如果 Safari 在此方面与标准不同,我不会感到惊讶。
在 iOS Safari 上无效,无法避免模态框打开时 body 滚动,即使 body 被背景覆盖。
似乎 Apple 在这里做了一些奇怪的事情,就像 Mac 上的 Safari 不支持
<input type='date'/>
(支持类型,但没有行为),但 iOS 上的 Safari 支持一样。有一个非常轻量级的 npm 包可以很好地处理这个问题。
https://npmjs.net.cn/package/body-scroll-toggle
它基本上完全按照您描述的那样执行,但在更改它们之前还会保留现有的 body 样式。
看起来很棒。谢谢。
但是如果我想在模态框包含大量内容时使其可滚动呢?
或者只使模态框主体可滚动并设置最大高度。
我敢打赌,那些 15px 的滚动条宽度在跨浏览器方面不会起作用。你可能想要使用
overflow:scroll
,它将强制显示一个(始终处于非活动状态,因为你设置了高度)滚动条,或者根据 https://aykevl.nl/2014/09/fix-jumping-scrollbar 中的calc(100vw – 100%)
执行某些操作。是的,你是对的。我从 Bootstrap 中学习了这个
15px
,它在 Chrome 和 Safari 上有效。很高兴看到有人关注一个不应该被忽视的实际问题。此解决方案还考虑了 Safari 滚动条的大小调整和设备旋转:https://radogado.github.io/natuive/#modal-window
position: fixed;
用于body
元素在基本演示中运行良好,但在移动设备上的真实网页中存在性能问题。在使用 fancybox 时,我以这种方式学习了这一点。还有非常流行的 Body scroll lock (BSL) npm 包
https://github.com/willmcpo/body-scroll-lock
我不满意你认为滚动条一定存在的假设。macOS 中的默认设置是根本没有滚动条;至少没有占用空间的滚动条,因此不需要在右侧添加填充。
David Walsh 有一个巧妙的方法来找出需要多少填充!
https://davidwalsh.name/detect-scrollbar-width
关键在于
someDiv.offsetWidth
,它会给出包含垂直滚动条的宽度。然后将其与someDiv.clientWidth
进行比较,后者不包含滚动条,瞧,你就知道你需要知道什么了!以下是 David 文章中的代码
Safari 刚刚修复了滚动主体问题,因此你不再需要
position: fixed
了这条评论让我的一天都变得美好。谢谢!
在一个快速演示中
在我的 iOS 13.5.1 手机上,主体仍然可以滚动。
过度滚动行为,虽然还没有得到 100% 的支持,但基本上是为这个用例而设计的。
一旦元素到达其滚动容量的末端,它就会阻止元素内的任何滚动向上冒泡。
https://caniuse.cn/#feat=css-overscroll-behavior
也就是说,
overscroll-behavior: contain
。似乎这个变量的名称需要是“scrollY”,而不是“top”?
const top = document.body.style.top;
https://github.com/willmcpo/body-scroll-lock
强烈推荐。
嗨,Brad!我不会说“其实”,只是想感谢你撰写了这篇优秀的文章。这篇文章对构建一个 iOS 友好的模态组件非常有帮助。
我也想分享一下。移动 Safari 会导致一些真正的滚动问题,尤其是在你的模态框有滚动并且背景也有滚动的时候。
相关:https://stackoverflow.com/questions/41594997/ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-posi
这就是为什么我不使用 hack 的原因……感谢更新。
我同意,
overscroll-behavior: contain;
是解决这个问题的答案。其他所有方法似乎都是 hack。即使它没有得到完全支持,并且并不总是适用(其他评论提到了边缘情况)。
边缘情况需要 hack 以获得更广泛的支持,但不需要用于最佳解决方案。几年后,此页面将过时,而实际答案需要成为首先展示的内容,而不是 hack。
嗨,Brad Wu!
我使用了你的代码并解决了我的问题。特别感谢。:)
// 当模态框显示时,我们想要一个固定的主体
document.body.style.position = ‘fixed’;
document.body.style.top =
-${window.scrollY}px
;在我的 Chrome(OSX)上,如果你按照此顺序操作,
position: fixed
会将滚动重置为 0。你需要先设置top
值,然后设置position: fixed
,否则你的滚动会在从window.scrollY
读取之前重置为 0。也许这已经在评论中提到了,但如果脚本在主体已经固定时激活,它会将滚动跳转到页面顶部。在我的例子中,这是因为我有一些导航选项会激活 mega-dropdown,并且使用了这个脚本。
谁能帮我解决 ImgMod 的问题:我希望它可以通过鼠标滚轮滚动,而不是滚动条,因此主体是固定的!我无法使用给定的示例实现它。谢谢。