Open scottaohara opened 2 years ago
Thank you Scott for raising the issue here.
If scrolling the underlying document is to be prevented, I think to be thorough it would also need to be prevented in any child scrollable element not in the modal dialog.
Scott's demo includes an option, Enable overscroll-behavior: contain
, an example of how the issue could be mitigated. I've found in Firefox (v98.0.1 for Mac) the property doesn't prevent scrolling at all and it doesn't always prevent scrolling in Chrome (v99.0.4844.83 for Mac). As mentioned in the demo, it doesn't prevent keyboard keys from scrolling the document; since it also doesn't prevent clicking and dragging the document scrollbar, this is more equitable behavior (the dialog ::backdrop
prevents use of scrollbars within the document).
A well-designed modal dialog should include whatever information is needed to complete the intended tasks within. Nevertheless, there could be situations where it's beneficial to users who can see the underlying document to be able to still scroll it: A product in a dialog shopping cart can be compared to products listed in the underlying page; while filling out a form in a dialog, someone with difficulty recalling information from the page may be able to scroll it into view. All of this is dependent on how visible the document is through the ::backdrop
and what the modal dialog is covering.
Is blocking scrolling something we should special case for <dialog>
? or should we do this to all nodes that are inert?
I think forcing overflow: hidden
may be a bit disruptive for websites where some content is expected to be visible just with overflow: visible
, especially if done for all inert nodes. We may be able to force overscroll-behavior: contain
though I don't know how this solves this particular issue.
Special casing <dialog>
isn't particularly great IMO.
@nt1m good point about not wanting to special case, completely agree.
There definitely could be benefit to doing this for other inert nodes, especially if they represent other types of 'popups' where focus navigation should be trapped to the popup so long as it is invoked - and thus accidentally scrolling the underlying document in those cases could either result in an unwanted dismissal of the popup, or a visual separation of the popup from its invoking element.
But, maybe this is more an explicit opt in for authors? Whether that be an html feature to do so, or author guidance on how to do this to in a standardized way. I can think of both situations where I'd absolutely want this, and others where it'd be less than ideal.
Just hit this issue and wondered what the current consensus is? Arrived here having done what I consider the 'common sense' thing and applied overscroll-behavior: contain
to the dialog to ensure the trigger for what caused the dialog in the first place remains in situ.
Is the current situation that there is no 'proper' way to prevent overscroll with dialog elements?
This has also come up for me today. I followed Adam Argyles excellent article : https://web.dev/building-a-dialog-component/
Although it;s come back that they expected the whole page to scroll rather than content inside the modal/dialog. I guess the default behaviour is inert which blocks focus and click events. It could be argued you are no longer interacting with whats behind the modal so it shouldn't scroll.
Also fromthe w3c
. Windows under a modal dialog are inert. That is, users cannot interact with content outside an active dialog window. Inert content outside an active dialog is typically visually obscured or dimmed so it is difficult to discern, and in some implementations, attempts to interact with the inert content cause the dialog to close. A
Adam just responded on twitter
Scrolling shouldn't happen behind the modal, things behind the modal should be considered inert / hidden
CSS provides a way to do this:
dialog {
overflow: auto;
overscroll-behavior: contain;
}
This should work in browsers, but it doesn’t because they all have the same bug: https://github.com/w3c/csswg-drafts/issues/3349#issuecomment-492721871.
I think the best path forwards is for browsers to fix this bug.
This should work in browsers, but it doesn’t because they all have the same bug: w3c/csswg-drafts#3349 (comment).
But will it work also, when the pointer is over the backdrop area and the mouse wheel is used? Because currently, even when the dialog is scrollable and the overscroll-behavior blocks the scrolling outside when my cursor is over the dialog area, it doesn't block when the cursor is over the backdrop area.
Also for scrolling with keyboard it doesn't seem to work at all.
@pepkin88 When I do this:
dialog, ::backdrop {
overscroll-behavior: contain;
}
attempting to scroll while the cursor is over the backdrop:
Test page: https://output.jsbin.com/puwojoy/quiet
I assume Chrome’s implementation is acceptable. In that case, we should investigate why the other browsers behave differently and maybe try to get them to align with Chrome.
Regarding scrolling with the Arrow Up and Down keys while the dialog is open (same test page):
overscroll-behavior: contain
is ignored)This seems to be a five-year-old Chromium bug:
https://bugs.chromium.org/p/chromium/issues/detail?id=824555
Interesting.
I checked this test page. On Chromium, it seems that when my cursor is over the backdrop pointing on the hr
element, the outer scroll is blocked. But when the cursor is not over hr
, e.g. is over the p
element, the page is scrolling. Weird.
I can confirm. The same happens when hovering the ”tab stop” inputs. This could be another Chromium bug.
I updated the test page from <hr size=100>
to <hr style="margin-block: 50px">
to make it easier to reproduce page scrolling in Chrome.
@simevidas can confirm using your https://output.jsbin.com/puwojoy/quiet test page the following when on Firefox (Linux):
This is a comment from the Accessible Platform Architectures (APA) Working Group.
The APA WG supports this proposal; it would improve accessibility for people who are experiencing a range of vision and cognition barriers.
In our discussion, one of our members (@AutoSponge) suggested that animations that are happening on the page behind the dialog should be paused; we feel that would help accessibility too. (That would be a separate discussion, but I mention it here to gauge interest; we'd be happy to file a new issue.)
This is a very common issue in the webdev with dozens of hacks and no good solution. I was hoping that once we will be able to use native dialog
it would finally go away, but it appears it is still present. I realize that it's mostly not dialog's fault, but rather two bugs [1] [2] that are there forever. But it still would be nice if dialog
had it solved. After all "modal" seems to impose that dialog is the only piece of the page I'm able to interact with, at least that's what I thought.
Want to add a +1 to this idea. Every time I've ever implemented modal dialogs I've had to add JavaScript to add overflow: hidden to the html element when open and remove it when closed. It'd be nice if this was automatically handled by dialog (or inert more generally)
I tried to build with <dialog>
for the first time today (in Chrome), then I hit this, and found this issue.
There is no robust reliable way prevent the issue (at least in Chrome). f.e.
overscroll-behavior: contain
does nothing if the dialog is not a scroll container, and it is even more odd when the dialog is full screen and content below still scrollsinert
attribute on document.body
is ignored (EDIT: Oh, that doesn't prevent scroll regardless)The only way to do it is non-robustly: f.e. set overflow: hidden
on the scrolling ancestor, etc, but this can break other people's styling (f.e. the modal dialog is made by a library component and should not touch other DOM).
The out-of-the-box behavior (at least in Chrome) is not a great UX.
Is there a Chromium issue tracking this specifically? I didn't see one, but maybe I didn't search well enough.
I’m linking the Chromium bug re overscroll-behavior: contain
not working for dialogs without overflow:
https://bugs.chromium.org/p/chromium/issues/detail?id=813094
u can fix this scrolling with the following CSS in the global CSS file: body:has(dialog[open]) { overflow: hidden; }
u can fix this scrolling with the following CSS in the global CSS file: body:has(dialog[open]) { overflow: hidden; }
Yes, it's a partial fix, but it's not perfect, because it hides the scrollbar, which may cause layout shifts.
Yes, it's a partial fix, but it's not perfect, because it hides the scrollbar, which may cause layout shifts.
I can confirm layout shifts when the scrollbar is hidden, which I was hoping to get rid of using the native dialog component. I'm honestly surprised theres no way yet to control the scroll behavior.
You can use scrollbar-gutter: stable to prevent the layout shift.
scrollbar-gutter: stable
is a decent solution, but the downside is that I can't control what is rendered on the gutter. I tried covering it somehow, but it doesn't work.
To fix that issue you can use this trick for now:
.main-wrapper {
padding-left: calc(100vw - 100%);
}
It applies the same padding on the left, as the scrollbar does on the right when present.
Unfortunately most of the time you can not apply this directly to body, as full width children like navbars would no longer work out of the box. Hence the .main-wrapper
.
u can fix this scrolling with the following CSS in the global CSS file: body:has(dialog[open]) { overflow: hidden; scrollbar-gutter: stable; }
Something that came up from a discussion on this recently is this won't work if the dialog is inside a shadow tree as the has won't pierce the boundary.
I will add, just because it is rarely mentioned, that this also may cause a jump to the vertical beginning of the document. If a model is opened further down the document and overflow: hidden;
is set to the body or <html>
, readers may be yoinked to the beginning of the document.
A reliable way to prevent scrolling in place in case that a ::backdrop
or top layer is visible would be much appreciated.
I'm using this function I found in melt-ui, and it works great in my case https://github.com/melt-ui/melt-ui/blob/f15cd300735c1abe95286d32956dfe3b403f9f0d/src/lib/internal/helpers/scroll.ts#L39
Bit of a hack, but thought I'd share what. I ended up doing.
body:has(dialog[open]) {
overflow: hidden;
}
The overflow:hidden
hack has been mentioned quite a few times in this thread already :)
It's a good workaround for now, but it's hacky and can cause some negative effects, which have also been pointed out many times in the thread.
Hopefully there's a cleaner solution soon!
This worked for me.
html:has(dialog[open]) {
overflow: hidden;
}
Clearly this feature is needed.
Although, overflow: hidden
won't work due to layout shifts.
So, should there be a new CSS property to prevent page scroll with the scroll bar showing? Should an issue be opened there?
Like what are we waiting on for this to be standardised
To be fair, the proper fix for this issue is known. Here are the steps we need to address this:
overscroll-behavior: contain
on the dialogoverscroll-behavior
for elements with no overflow (Chrome bug, Firefox bug, Safari bug)overscroll-behavior
for keyboard scroll with arrows, space and page up/down buttons (Chrome bug, Firefox ✅, Safari ✅)EDIT: Call me crazy, but I address this issue in my code by adding a little wrapper around my actual dialog content, making it scrollable with hidden scrollbar and then making my dialog content sticky inside so this scroll is not noticeable for the user. This way I mitigate all the issues above and lock scroll inside the dialog even if there's not enough content to overflow.
To be fair, the proper fix for this issue is known. Here are the steps we need to address this: [...]
- Browsers ignoring
overscroll-behavior
for elements with no overflow (Chrome bug, Firefox bug, Safari bug)
Hi! Firefox engineer here. This comment prompted me to have a look at what it would take to fix this Firefox bug, and I realized something: when we rolled out our Site Isolation feature, we inadvertently changed our behaviour to align with the spec (i.e. respect overscroll-behavior
on scroll containers with no overflow) in a subset of cases (namely, the viewports of cross-origin iframes).
We promptly got bug reports of the following form: "I'm scrolling an article with some Twitter embeds. If my mouse happens to land on top of a Twitter embed, I can no longer scroll the page (without moving the mouse away from the embed)", and had to patch this so we continued to ignore overscroll-behavior
on iframes with no overflow.
This is making me wonder whether it's realistic / web compatible to change this behaviour to align exactly with what the current spec says. Maybe we need to revisit the spec and have a carve-out for respecting overscroll-behavior
across origins?
@theres-waldo Would it be possible to avoid this issue by changing how the mouse cursor interacts with iframes in the following way:
I’m suggesting this because it’s a behavior that I wanted for a long time. Not just for iframes but also for elements with hover actions. For example, if I scrolled my Twitter timeline and after the scrolling stopped, my mouse cursor happened to end up being over a user avatar, Twitter would show the user info popup. That’s just annoying. I didn’t hover the avatar intentionally. I just scrolled the page. My point is that moving the cursor and scrolling the page are two different user actions with completely different intentions. If the mouse cursor ends up over an element after a scroll operation, there should be no action.
So if the user scrolls the page and the mouse cursor is temporarily over an iframe, there should be no interaction with the iframe. Because the user did not intend to interact with the iframe. The user’s intention was to scroll the page, so the page should continue scrolling.
@theres-waldo Would it be possible to avoid this issue by changing how the mouse cursor interacts with iframes in the following way:
If the user moves the mouse cursor to a part of the page and then starts scrolling, the page scrolls.
If the mouse cursor goes over an iframe while the page is scrolling, nothing changes. The page continues scrolling.
If the user explicitly moves the mouse cursor into an iframe and then starts scrolling, the scroll action is performed on the iframe. (Of course, scrolling can then chain up to the outer page if the iframed page does not prevent it.)
Firefox actually does a time-limited version of this called "wheel transactions". Wheel events that occur in succession without a mouse-move in between are grouped into a "transaction", and the scroll target chosen for the first event in the transaction is propagated to subsequent events Transactions do "time out" after 1.5 seconds though, so if more time than that elapses between wheel events then the new event will pick up a new scroll target even if no mouse-move has occurred in between.
So, what you describe already works if you continue scrolling relatively quickly after the mouse lands on the iframe.
Did you envision this behaviour, but without a timeout (so, mouse lands on iframe, you get up from your desk to get a coffee, you come back and continue scrolling, and the browser still remembers that the page scrolled last so it should scroll rather than the iframe)? Or with a timeout that's significantly longer than 1.5s?
I’m suggesting this because it’s a behavior that I wanted for a long time. Not just for iframes but also for elements with hover actions. For example, if I scrolled my Twitter timeline and after the scrolling stopped, my mouse cursor happened to end up being over a user avatar, Twitter would show the user info popup. That’s just annoying. I didn’t hover the avatar intentionally. I just scrolled the page.
I frequently find accidental hovers like this to be annoying as well.
However, in this case, our hands may be tied by the spec. If I understand correctly, such hovers are typically triggered by mouseover
(or mouseenter
) and the spec says (emphasis mine):
A user agent MUST dispatch this event when a pointing device is moved onto the boundaries of an element or when the element is moved to be underneath the primary pointing device.
A user agent MUST dispatch this event when a pointing device is moved onto the boundaries of an element or when the element is moved to be underneath the primary pointing device.
This may simply be a matter of perspective then. You could posit that the pointer doesn't exist while panning/scrolling in the same way that it doesn't exist while dragging a scrollbar manually.
u can fix this scrolling with the following CSS in the global CSS file: body:has(dialog[open]) { overflow: hidden; }
GENIUS! That fixed it, sending thanks!
The only slight issue still, is that this removes the scrollbars when the modal is open, so there is a brief layout shift whilst it opens.
@theres-waldo The 1.5 second timeout sounds good. I’ve had problems in Firefox in the past where scrolling would stop immediately when the mouse cursor got over a specific element on the page, such as a Google map. Maybe that issue was fixed in the meantime.
@theres-waldo The 1.5 second timeout sounds good. I’ve had problems in Firefox in the past where scrolling would stop immediately when the mouse cursor got over a specific element on the page, such as a Google map. Maybe that issue was fixed in the meantime.
Ah, yeah, that's a known bug, and it's being fixed (bug 1888946).
Maybe I please request in future that when there is a option, e.g. to allow content behind the backdrop to be scrolled or not scrolled, allow both behaviors to be achieved by somemeans. There are people on both sides of the fence who will need both situations. As web developers often we have our hands tied. The dialog and popover are prime examples of APIs/Element that could be so helpful, but due to various gotchas and assumptions, are not usable beyond toy examples.
Is blocking scrolling something we should special case for
<dialog>
? or should we do this to all nodes that are inert?I think forcing
overflow: hidden
may be a bit disruptive for websites where some content is expected to be visible just withoverflow: visible
, especially if done for all inert nodes. We may be able to forceoverscroll-behavior: contain
though I don't know how this solves this particular issue.Special casing
<dialog>
isn't particularly great IMO.
The top layer is a special case. Doesn't get much more special case than the top layer.
To be fair, the proper fix for this issue is known. Here are the steps we need to address this: [...]
- Browsers ignoring
overscroll-behavior
for elements with no overflow (Chrome bug, Firefox bug, Safari bug)Hi! Firefox engineer here. This comment prompted me to have a look at what it would take to fix this Firefox bug, and I realized something: when we rolled out our Site Isolation feature, we inadvertently changed our behaviour to align with the spec (i.e. respect
overscroll-behavior
on scroll containers with no overflow) in a subset of cases (namely, the viewports of cross-origin iframes).We promptly got bug reports of the following form: "I'm scrolling an article with some Twitter embeds. If my mouse happens to land on top of a Twitter embed, I can no longer scroll the page (without moving the mouse away from the embed)", and had to patch this so we continued to ignore
overscroll-behavior
on iframes with no overflow.This is making me wonder whether it's realistic / web compatible to change this behaviour to align exactly with what the current spec says. Maybe we need to revisit the spec and have a carve-out for respecting
overscroll-behavior
across origins?
Well, how about we fix the bugs instead of treating them as features, and the spec progresses. Backwards compatiability is important, but when relying on certain bugs as features, thats going back to IE6/7/8/9 days.
How about a new overscroll-behavior
value trap
:
overscroll-behavior: trap
- traps all scoll events to itself, whether it's scrollable or not, that dont go beyond that element when a scroll event occurs over it.I also just ran into this issue and was surprised there isn't a native way to prevent the background from scrolling (yet).
The following function is inspired by Brad Wu's article here: https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/ as well as the code from melt-ui here: https://github.com/melt-ui/melt-ui/blob/f15cd300735c1abe95286d32956dfe3b403f9f0d/src/lib/internal/helpers/scroll.ts#L39.
I believe this should work for most use cases until we have a native solution. This function could be improved by storing the current CSS props/values of the body element before locking. You would then restore these props/values when unlocking.
Call scroll_lock()
to lock the background scroll.
Call scroll_lock(false)
to unlock the background scroll.
/**
* Lock or unlock the body scroll.
*
* @param {boolean} lock Whether to lock or unlock the body scroll. Default = true.
*/
const scroll_lock = (lock = true) => {
const { documentElement, body } = document;
// RTL <body> scrollbar
const documentLeft = documentElement.getBoundingClientRect().left;
const scrollbarX = Math.round(documentLeft) + documentElement.scrollLeft;
const paddingProp = scrollbarX ? 'paddingLeft' : 'paddingRight';
if (lock) {
body.style[paddingProp] = `${window.innerWidth - documentElement.clientWidth}px`;
body.style.top = `-${window.scrollY}px`;
body.style.left = `-${window.scrollX}px`;
body.style.right = 0;
body.style.position = 'fixed';
body.classList.add('scroll-locked');
} else {
if ( ! body.classList.contains('scroll-locked') ) {
return;
}
const currentScrollY = parseInt(body.style.top || '0') * -1;
const currentScrollX = parseInt(body.style.left || '0') * -1;
body.style[paddingProp] = '';
body.style.position = '';
body.style.top = '';
body.style.left = '';
body.style.right = '';
body.classList.remove('scroll-locked');
window.scrollTo(currentScrollX, currentScrollY);
}
}
Another UX/a11y improvement for the
dialog
element would be to ensure that, when in the modal state, the underlying document cannot be scrolled. This will help ensure that the invoking element for the dialog will remain in the visible viewport, and when focus is returned to the document, the viewport will not have to re-scroll to ensure this element is in view. This topic was originally surfaced by Curtis Wilcox on the web a11y slack channel.I have created a quick demo to show how the underlying document is presently scrollable while the modal dialog is visible: https://codepen.io/scottohara/full/YzYGpNy
One caveat I can think of with this idea is that if an author makes a dialog that is too large for the current viewport, then the underlying document will need to be able to scroll so as to allow someone to view all the content of the dialog.
Though, one could also argue that this could be, at least partially, mitigated by setting a max-height/width to the dialog so that it does not extend beyond the bounds of the browser viewport. Authors will need to do something like this to some degree to meet wcag success criterion 1.4.10: Reflow.