{"id":6397,"date":"2025-07-16T08:13:36","date_gmt":"2025-07-16T13:13:36","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=6397"},"modified":"2025-07-16T08:13:37","modified_gmt":"2025-07-16T13:13:37","slug":"adaptive-alerts-a-css-scroll-state-use-case","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/adaptive-alerts-a-css-scroll-state-use-case\/","title":{"rendered":"Adaptive Alerts (a CSS scroll-state Use Case)"},"content":{"rendered":"\n<p>Sometimes it\u2019s useful to adapt the controls available to users based on whether they\u2019ve <strong>scrolled through key position<\/strong>s on a page.<\/p>\n\n\n\n<p>Here&#8217;s an example: a user scrolls through a Terms &amp; Conditions page. If they click \u201cagree\u201d <em>without<\/em> having scrolled down until the end, we could prompt them with a &#8220;please confirm you&#8217;ve read these terms&#8221; before continuing. Whereas if they <em>have<\/em> scrolled down the whole way, that could imply they have read the terms, so we don&#8217;t need the additional prompt. <\/p>\n\n\n\n<p>Implementing something like this is relatively easy with the recent CSS scroll-state queries (<a href=\"https:\/\/frontendmasters.com\/blog\/wp-admin\/post.php?post=6397&amp;action=edit\">browser support<\/a>).<\/p>\n\n\n\n<p>The following is an example of exactly as described above. If you click the \u201cSign Up\u201d button without having scrolled down until the end, you\u2019ll see an additional prompt reminding that you might not have read the terms yet and if you\u2019d still like to sign up. And if the \u201cSign Up\u201d is clicked after the text has been scrolled to the end, the sign-up acknowledgement pops up without any confirmation prompt first.<\/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='362' src='https:\/\/videopress.com\/embed\/KZkpkHaK?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=1739540970'><\/script><\/div>\n\t\t\t<figcaption>This is a video version of the demo, because <a href=\"https:\/\/caniuse.com\/mdn-css_at-rules_container_scroll-state_queries\">browser support is Chrome-only<\/a> as this article is published.<\/figcaption>\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>Here&#8217;s a live demo:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_zxGyQNe\" src=\"\/\/codepen.io\/anon\/embed\/zxGyQNe?height=720&amp;theme-id=1&amp;slug-hash=zxGyQNe&amp;default-tab=result\" height=\"720\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed zxGyQNe\" title=\"CodePen Embed zxGyQNe\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-layout\">The Layout<\/h2>\n\n\n\n<p>We&#8217;ll start with this basic layout:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">article<\/span>&gt;<\/span>\n  <span class=\"hljs-comment\">&lt;!-- many paragraphs of ToS text goes here --&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"control\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span>&gt;<\/span>Sign Up<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">article<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\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\">article<\/span> {\n  <span class=\"hljs-attribute\">overflow<\/span>: scroll;\n  <span class=\"hljs-attribute\">container-type<\/span>: scroll-state;\n  .control {\n    <span class=\"hljs-attribute\">position<\/span>: sticky;\n    <span class=\"hljs-attribute\">bottom<\/span>: -<span class=\"hljs-number\">20px<\/span>;\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>The sign up button\u2019s container (<code>.control<\/code>) is a <em>sticky<\/em> element that sticks to the bottom of its scrollable container (<code>&lt;article><\/code>). This is so the user always has access to the sign up button, in case they prefer to drop reading the terms and sign up right away.<\/p>\n\n\n\n<p>The scrollable container (<code>&lt;article><\/code>) has <code>container-type: scroll-state.<\/code> This makes it possible to make changes to its descendants based on their scroll positions.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-scroll-state-conditional-rule\">The Scroll-State Conditional Rule<\/h2>\n\n\n\n<p>This is where we code in how the button control\u2019s action adapts to its scroll position inside the&nbsp;<code>article<\/code>.<\/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-keyword\">@container<\/span> <span class=\"hljs-keyword\">not<\/span> scroll-state(<span class=\"hljs-attribute\">scrollable:<\/span> bottom) {\n  <span class=\"hljs-selector-tag\">button<\/span> {\n    <span class=\"hljs-attribute\">appearance<\/span>: button;\n  }\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>When the container (<code>&lt;article><\/code> in our example) can no longer be scrolled further down, i.e. the container has already been scrolled until its bottom edge, we make a subtle change to the button in CSS that won\u2019t visually modify it. In the example above, the button\u2019s appearance is set to button from its default auto, keeping the button\u2019s look the same.\u00a0<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-alerts\">The Alerts<\/h2>\n\n\n\n<p>When the button is clicked, depending on the value of its <code>appearance<\/code> property, show the relevant alert.\u00a0<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'button'<\/span>).onclick = <span class=\"hljs-function\">(<span class=\"hljs-params\">e<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">if<\/span> (getComputedStyle(e.target).appearance == <span class=\"hljs-string\">\"auto\"<\/span> \n      &amp;&amp; !confirm(<span class=\"hljs-string\">\"Hope you've read the terms. Do you wish to complete the sign up?\"<\/span>))\n    <span class=\"hljs-keyword\">return<\/span>;\n\n  alert(<span class=\"hljs-string\">\"Sign up complete\"<\/span>);\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>If the <code>&lt;article><\/code> has not been scrolled down until the end, the button\u2019s <code>appearance<\/code> value remains its default auto (<code>getComputedStyle(e.target).appearance == \"auto\"<\/code>). The click handler executes a <code>confirm()<\/code> prompt reminding the user they might not have read the terms fully yet, and if they\u2019d like to continue with the sign up. If the user clicks \u201cOK\u201d, the <code>alert(\"Sign up complete\")<\/code> message shows up next.\u00a0<\/p>\n\n\n\n<p>If the article has been scrolled down to the end, the button will have an <code>appearance<\/code> value other than <code>auto<\/code>, and so the click handler executes the <code>alert()<\/code> <em>only<\/em>. \u00a0<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Learn about\u00a0scroll-state queries\u00a0(<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_conditional_rules\/Container_scroll-state_queries\">here<\/a> and\/or <a href=\"https:\/\/developer.chrome.com\/blog\/css-scroll-state-queries\">here<\/a>) to know the different kinds of scrolling scenarios that you can work with. Based on scroll states and positions, you\u2019ll be able to change the appearance, content, or even functionality (as seen in this article) of an element or module.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A single button, but it has two different behaviors in JavaScript depending on how far you\u2019ve scrolled in an element (as determined by CSS!)<\/p>\n","protected":false},"author":20,"featured_media":6559,"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":[371,372],"class_list":["post-6397","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-container-type","tag-scroll-state"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/Adaptive-Alerts-a-CSS-scroll-state-Use-Case.jpg?fit=1140%2C676&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6397","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\/20"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=6397"}],"version-history":[{"count":12,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6397\/revisions"}],"predecessor-version":[{"id":6561,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6397\/revisions\/6561"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/6559"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=6397"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=6397"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=6397"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}