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:
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. 🤷
Unfortunately, Chrome has a bug that prevents “scrollbar-gutter: stable” from affecting “position: fixed” elements: https://issues.chromium.org/issues/40792788
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 tooverflow-y: scroll
by default, which caused the body to keep scrolling even after applyingoverflow: hidden
to it.