Scroll-Locked Dialogs

I just wrote about the <dialog> element, with some basic usage and things to watch for. It’s a great addition to the web platform.

Here’s another interesting thing we can do, connecting it to another one of my favorite new things on the web platform: :has(). (You can see I’ve been pretty into it lately.) I was reading Locking scroll with :has() from Robb Owen, and he gets into how you might want to prevent the page from scrolling when a modal is open.

To prevent from losing the user’s place in the page whilst that modal is open – particularly on mobile devices – it’s good practice to prevent the page behind it from scrolling. That’s a scroll lock.

I like that. The user can’t interact with what’s behind the modal anyway, as focus is trapped into a modal (that’s what a modal is). So, might as well make sure they don’t inadvertently scroll away. Without doing anything, scrolling is definitely not locked.

My first thought was actually… I wonder if overscroll-behavior on the dialog (and maybe the ::backdrop too?) would prevent that, but some quick testing didn’t seem to work.

So to scroll lock the page, as Robb did in his article, we can do by hiding the overflow on the body. Robb did it like this:

body:has(.lock-scroll) {
  overflow: hidden;
}Code language: CSS (css)

Which would then lock if the body had a <dialog class="lock-scroll"> in it. That’s fine, but what I like about <dialog> is that it can sorta always be in the DOM, waiting to open, if you like. If you did that here, it would mean the scroll is always locked, which you certainly don’t want.

Instead, I had a play like:

body {
  ...

  &:has(dialog[open]) {
    overflow: hidden;
  }
}Code language: CSS (css)

So this just locks scrolling only when the dialog is open. When you call the API like showModal, it toggles that open attribute, so it’s safe to use.

This works, see:

But… check out that content shift above. When we hide the overflow on the body, there is a possibility (depending on the browser/platform/version/settings and “overlay” scrollbars) that the scrollbars are taking up horizontal space, and the removal of them causes content to reflow.

Fortunately, there is yet another modern CSS feature to save us here, if we determine this to be a problem for the site we’re working on: scrollbar-gutter. If we set:

html {
  scrollbar-gutter: stable;
}Code language: CSS (css)

Then the page will reserve space for that scrollbar whether there is scrolling or not, thus there will be no reflow when the scrollbar pops in and out. Now we’re cookin’ — see:

I don’t think scrollbar-gutter is a home run, sadly. It leaves a blank strip down the side of the page (no inherited background) when the scrollbar isn’t there, which can look awkward. Plus, “centered” content can look less centered because of the reserved space. Just one of those situations where you have to pick which is less annoying to you 🫠.

Demo:

Wanna learn CSS from a course?

3 responses to “Scroll-Locked Dialogs”

  1. Tyler Sticka says:

    I opened a CSSWG issue for managing which element controls scrolling back in 2020, but it hasn’t seen much activity: https://github.com/w3c/csswg-drafts/issues/4710

    I’m surprised and disappointed by that… I try to avoid modals and overlays as much as I can, but it’s silly for them to remain this challenging. 🤷

  2. Steffen says:

    Unfortunately, Chrome has a bug that prevents “scrollbar-gutter: stable” from affecting “position: fixed” elements: https://issues.chromium.org/issues/40792788

  3. Jano says:

    Love the simplicity of this technique! That said, applying it to the <html> element instead of the <body> might make it a bit more bulletproof, as I recently experienced on a page that had the <html> set to overflow-y: scroll by default, which caused the body to keep scrolling even after applying overflow: hidden to it.

Leave a Reply

Your email address will not be published. Required fields are marked *

Did you know?

Frontend Masters Donates to open source projects. $313,806 contributed to date.