{"id":938,"date":"2024-02-19T13:01:09","date_gmt":"2024-02-19T19:01:09","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=938"},"modified":"2024-09-12T10:20:25","modified_gmt":"2024-09-12T15:20:25","slug":"scroll-locked-dialogs","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/scroll-locked-dialogs\/","title":{"rendered":"Scroll-Locked Dialogs"},"content":{"rendered":"\n<p><a href=\"https:\/\/frontendmasters.com\/blog\/basic-dialog-usage-and-gotchas-to-watch-for\/\">I just wrote about the <code>&lt;dialog&gt;<\/code> element<\/a>, with some basic usage and things to watch for. It&#8217;s a great addition to the web platform. <\/p>\n\n\n\n<p>Here&#8217;s another interesting thing we can do, connecting it to another one of my favorite new things on the web platform: <code>:has()<\/code>. (You can see I&#8217;ve been <a href=\"https:\/\/frontendmasters.com\/blog\/tag\/has\/\">pretty into it<\/a> lately.) I was reading <a href=\"https:\/\/robbowen.digital\/wrote-about\/locking-scroll-with-has\/\">Locking scroll with :has()<\/a> from Robb Owen, and he gets into how you might want to prevent the page from scrolling when a modal is open.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>To prevent from losing the user&#8217;s place in the page whilst that modal is open \u2013 particularly on mobile devices \u2013 it&#8217;s good practice to prevent the page behind it from scrolling. That&#8217;s a scroll lock.<\/p>\n<\/blockquote>\n\n\n\n<p>I like that. The user can&#8217;t interact with what&#8217;s behind the modal anyway, as focus is trapped into a modal (that&#8217;s what a modal is). So, might as well make sure they don&#8217;t inadvertently scroll away. Without doing anything, scrolling is definitely not locked. <\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='254' src='https:\/\/videopress.com\/embed\/xMXjEUCP?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1674852142'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>My first thought was actually&#8230; I wonder if <code>overscroll-behavior<\/code> on the <code>dialog<\/code> (and maybe the <code>::backdrop<\/code> too?) would prevent that, but some quick testing didn&#8217;t seem to work. <\/p>\n\n\n\n<p>So to scroll lock the page, as Robb did in his article, we can do by hiding the <code>overflow<\/code> on the <code>body<\/code>. Robb did it like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">body<\/span><span class=\"hljs-selector-pseudo\">:has(.lock-scroll)<\/span> {\n  <span class=\"hljs-attribute\">overflow<\/span>: hidden;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Which would then lock if the body had a <code>&lt;dialog class=\"lock-scroll\"&gt;<\/code> in it. That&#8217;s fine, but what I like about <code>&lt;dialog&gt;<\/code> is that it can sorta <em>always<\/em> be in the DOM, waiting to open, if you like. If you did that here, it would mean the scroll is <em>always<\/em> locked, which you certainly don&#8217;t want. <\/p>\n\n\n\n<p>Instead, I had a play like:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">body<\/span> {\n  ...\n\n  &amp;:has(dialog&#91;open]) {\n    <span class=\"hljs-attribute\">overflow<\/span>: hidden;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>So this just locks scrolling <em>only<\/em> when the dialog is open. When you call the API like <code>showModal<\/code>, it toggles that <code>open<\/code> attribute, so it&#8217;s safe to use.<\/p>\n\n\n\n<p>This works, see:<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='289' src='https:\/\/videopress.com\/embed\/1tauOP9B?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1674852142'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>But&#8230; check out that <em>content shift<\/em> above. When we hide the overflow on the body, there is a possibility (depending on the browser\/platform\/version\/settings and &#8220;overlay&#8221; scrollbars) that the scrollbars are taking up horizontal space, and the removal of them causes content to reflow. <\/p>\n\n\n\n<p>Fortunately, there is yet another modern CSS feature to save us here, if we determine this to be a problem for the site we&#8217;re working on: <code><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/scrollbar-gutter\">scrollbar-gutter<\/a><\/code>. If we set:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">html<\/span> {\n  <span class=\"hljs-attribute\">scrollbar-gutter<\/span>: stable;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>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&#8217;re cookin&#8217; \u2014 see:<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='273' src='https:\/\/videopress.com\/embed\/4GBLkCBq?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1674852142'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>I don&#8217;t think <code>scrollbar-gutter<\/code> is a home run, sadly. It leaves a blank strip down the side of the page (no inherited background) when the scrollbar isn&#8217;t there, which can look awkward. Plus, &#8220;centered&#8221; 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 \ud83e\udee0.<\/p>\n\n\n\n<p>Demo:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_QWooqVa\" src=\"\/\/codepen.io\/anon\/embed\/QWooqVa?height=450&amp;theme-id=47434&amp;slug-hash=QWooqVa&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed QWooqVa\" title=\"CodePen Embed QWooqVa\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n","protected":false},"excerpt":{"rendered":"<p>I just wrote about the &lt;dialog&gt; element, with some basic usage and things to watch for. It&#8217;s a great addition to the web platform. Here&#8217;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&#8217;ve been pretty into it lately.) [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":948,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[40,7,98,92],"class_list":["post-938","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-has","tag-css","tag-dialog","tag-scrolling"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/02\/scroll-lock-thumb.jpg?fit=1000%2C500&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/938","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=938"}],"version-history":[{"count":5,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/938\/revisions"}],"predecessor-version":[{"id":3815,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/938\/revisions\/3815"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/948"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=938"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=938"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=938"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}