Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider preventing page scroll when modal dialog is visible #7732

Open
scottaohara opened this issue Mar 21, 2022 · 37 comments
Open

Consider preventing page scroll when modal dialog is visible #7732

scottaohara opened this issue Mar 21, 2022 · 37 comments
Labels
a11y-tracker Group bringing to attention of a11y, or tracked by the a11y Group but not needing response. accessibility Affects accessibility topic: dialog The <dialog> element. topic: rendering

Comments

@scottaohara
Copy link
Collaborator

scottaohara commented Mar 21, 2022

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.

@annevk annevk added topic: dialog The <dialog> element. topic: rendering accessibility Affects accessibility a11y-tracker Group bringing to attention of a11y, or tracked by the a11y Group but not needing response. labels Mar 22, 2022
@extra808
Copy link

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.

@nt1m
Copy link
Member

nt1m commented Mar 24, 2022

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.

@scottaohara
Copy link
Collaborator Author

@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.

@benfrain
Copy link

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?

@trickydisco78
Copy link

trickydisco78 commented Nov 3, 2022

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

@simevidas
Copy link

simevidas commented Jun 6, 2023

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: w3c/csswg-drafts#3349 (comment).

I think the best path forwards is for browsers to fix this bug.

@pepkin88
Copy link

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.

@simevidas
Copy link

simevidas commented Jun 27, 2023

@pepkin88 When I do this:

dialog, ::backdrop {
  overscroll-behavior: contain;
}

attempting to scroll while the cursor is over the backdrop:

  • Chrome: scrolls the dialog
  • Safari and Firefox: scrolls the page

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):

  • Chrome: page starts scrolling after dialog scrolls to end (overscroll-behavior: contain is ignored)
  • Safari and Firefox: page does not start scrolling in this case

This seems to be a five-year-old Chromium bug:

https://bugs.chromium.org/p/chromium/issues/detail?id=824555

@pepkin88
Copy link

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.

@simevidas
Copy link

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.

@markcellus
Copy link

markcellus commented Jun 27, 2023

@simevidas can confirm using your https://output.jsbin.com/puwojoy/quiet test page the following when on Firefox (Linux):

  • When cursor is on the page, scroll wheel scrolls the page
  • When cursor is on modal, scroll wheel scrolls the dialog

@matatk
Copy link
Contributor

matatk commented Jul 5, 2023

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.)

@waterplea
Copy link

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.

@lukewarlow
Copy link
Member

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)

@trusktr
Copy link

trusktr commented Dec 30, 2023

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 scrolls
  • iframes ignore overscroll-behavior,
  • preventingDefault on wheel and other events works in some cases but not all, etc, and getting these handlers into iframes may even be impossible
  • inert attribute on document.body is ignored (EDIT: Oh, that doesn't prevent scroll regardless)
  • etc

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.

@simevidas
Copy link

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

@ciprianmacovei
Copy link

u can fix this scrolling with the following CSS in the global CSS file:
body:has(dialog[open]) {
overflow: hidden;
}

@pepkin88
Copy link

pepkin88 commented Jan 9, 2024

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.

@katerlouis
Copy link

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.

@lukewarlow
Copy link
Member

lukewarlow commented Mar 10, 2024

You can use scrollbar-gutter: stable to prevent the layout shift.

@pepkin88
Copy link

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.

@webbertakken
Copy link

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.

@lukewarlow
Copy link
Member

lukewarlow commented Mar 29, 2024

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.

@nachtfunke
Copy link

nachtfunke commented Apr 18, 2024

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.

@xob0t
Copy link

xob0t commented May 1, 2024

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

@BritishWerewolf
Copy link

Bit of a hack, but thought I'd share what. I ended up doing.

body:has(dialog[open]) {
    overflow: hidden;
}

@markcellus
Copy link

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!

@IlungaNtita
Copy link

This worked for me.

html:has(dialog[open]) {
  overflow: hidden;
}

@YummyBacon5
Copy link

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

@waterplea
Copy link

waterplea commented Jun 7, 2024

To be fair, the proper fix for this issue is known. Here are the steps we need to address this:

  1. overscroll-behavior: contain on the dialog
  2. Browsers ignoring overscroll-behavior for elements with no overflow (Chrome bug, Firefox bug, Safari bug)
  3. Browsers ignoring 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.

@theres-waldo
Copy link

To be fair, the proper fix for this issue is known. Here are the steps we need to address this:
[...]
2. 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?

@simevidas
Copy link

simevidas commented Jun 22, 2024

@theres-waldo Would it be possible to avoid this issue by changing how the mouse cursor interacts with iframes in the following way:

  1. If the user moves the mouse cursor to a part of the page and then starts scrolling, the page scrolls.
  2. If the mouse cursor goes over an iframe while the page is scrolling, nothing changes. The page continues scrolling.
  3. 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.)

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
Copy link

@theres-waldo Would it be possible to avoid this issue by changing how the mouse cursor interacts with iframes in the following way:

  1. If the user moves the mouse cursor to a part of the page and then starts scrolling, the page scrolls.

  2. If the mouse cursor goes over an iframe while the page is scrolling, nothing changes. The page continues scrolling.

  3. 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.

@austinw-fineart
Copy link

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.

@jamesgulland-nodelondon
Copy link

jamesgulland-nodelondon commented Jun 28, 2024

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.

@simevidas
Copy link

@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
Copy link

@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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a11y-tracker Group bringing to attention of a11y, or tracked by the a11y Group but not needing response. accessibility Affects accessibility topic: dialog The <dialog> element. topic: rendering
Development

No branches or pull requests