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
:hasoperator 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.