Disable scrolling for a modal dialog: my first use of CSS `:has`
Published on .The CSS :has()
pseudo-class makes it possible (and straightforward) to prevent scrolling on a page while a dialog
is open
. This week I got to apply this technique which was my first ever use of :has()
!
Here's the full snippet as a one-liner:
body:has(#my-modal[open]) { overflow: hidden }
Let's break it all down.
The dialog
element as a modal #
I have a dialog
element that is opened via the showModal()
method.
<dialog id="my-modal">
Modal content here
</dialog>
<script>
document.getElementById("my-modal").showModal();
</script>
Calling showModal()
on the dialog
automatically adds the open
attribute to it.
<!-- added attribute
↓↓↓↓ -->
<dialog id="my-modal" open>
Modal content here
</dialog>
Which allows us to select the dialog
specifically when it's open:
#my-modal[open] {
/* styles applied to the dialog
only for when the dialog is open */
}
Critically, #my-modal[open]
is a conditional that essentially means "when #my-modal
is open".
Naturally, when the modal dialog is closed, the open
attribute is automatically removed, disabling the selector above.
Modal dialog
does not prevent page-scrolling #
A modal dialog
automatically moves the focus to the content inside the dialog
and "traps" it there, which is exactly what a modal UI should do.
However, even when a dialog
is open as a modal, the rest of the page — what's "underneath" the modal — can still be scrolled through normally, which is not always what you want.
Prevent page-scrolling #
The way you would normally disable scrolling on a page is by applying overflow: hidden
to the body
or html
element.
body { overflow: hidden }
But we only want to do this when the dialog
is open! And this dialog element can be anywhere on the page (inside the body).
:has()
as a "global observer" #
By attaching :has
to the body
selector we're enabling a pattern where the styles will only be applied to the body
when the "sub-selector" inside the parens in :has()
is active.
In other words, the body
element is now observing for the "condition" defined in the sub-selector to be true in order to apply the styles, and it's doing this observation for any element inside itself.
body:has(conditional-selector) {
/* styles applied to body
only when the condition is true */
}
All together now #
At last, we have:
- The styles we want want to apply to the body to disable page-scrolling:
overflow: hidden
. - The conditional that represents when we want to apply those styles to the body; "when this specific modal is open":
#my-modal[open]
. - The
:has
operator that connects both and makes it all work.
The result (same as at the beginning of this post, but broken down in parts):
body
:has( /* 3 */
#my-modal[open] /* 2 */
) {
overflow: hidden /* 1 */
}
That's it ✨
This is a super flexible technique to have as part of your tool-belt. Of course it doesn't have to be a "global observer" only; it can be scoped down into a smaller section or component of a page, and any element attribute can be used as a conditional.
The possibilities with this usage of :has
are endless.