F = keyboard shortcuts
A Shower Of Thoughts

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.

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:

  1. The styles we want want to apply to the body to disable page-scrolling: overflow: hidden.
  2. The conditional that represents when we want to apply those styles to the body; "when this specific modal is open": #my-modal[open].
  3. 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.

💌 Reply to this post
More posts related to: development css protip