{"id":4277,"date":"2024-10-29T07:45:59","date_gmt":"2024-10-29T12:45:59","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=4277"},"modified":"2024-11-21T13:22:37","modified_gmt":"2024-11-21T18:22:37","slug":"scroll-driven-sections","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/scroll-driven-sections\/","title":{"rendered":"Scroll-Driven&#8230; Sections"},"content":{"rendered":"\n<p>I was checking out <a href=\"https:\/\/www.quantamagazine.org\/the-thought-experiments-that-fray-the-fabric-of-space-time-20240925\/\">a very cool art-directed article<\/a> the other day, full of <em>scrollytelling<\/em>, and, like us web devs will be forever cursed to do, wondering what they used to build it. Spoiler: it&#8217;s <a href=\"https:\/\/gsap.com\/\">GSAP<\/a> and <a href=\"https:\/\/gsap.com\/docs\/v3\/Plugins\/ScrollTrigger\/\">ScrollTrigger<\/a>. <\/p>\n\n\n\n<p>No shame in those tech choices, they are great. But with <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_scroll-driven_animations\">scroll-driven animations<\/a> now being a web standard with <a href=\"https:\/\/caniuse.com\/mdn-css_properties_animation-timeline_scroll\">growing support<\/a>, it begs the question whether we could do this with native technologies.<\/p>\n\n\n\n<p>My brain focused on one particular need of the scrollytelling style:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>While the page scrolls <em>through a particular section<\/em><\/li>\n\n\n\n<li>Have a child element appear in a fixed position <em>and be animated<\/em><\/li>\n\n\n\n<li>&#8230; but before and after this section is being scrolled through, the element is hidden<\/li>\n<\/ol>\n\n\n\n<p>Perhaps a diagram can help drive that home:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"462\" height=\"1024\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/10\/Frame-3.png?resize=462%2C1024&#038;ssl=1\" alt=\"\" class=\"wp-image-4278\" style=\"width:371px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/10\/Frame-3.png?resize=462%2C1024&amp;ssl=1 462w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/10\/Frame-3.png?resize=135%2C300&amp;ssl=1 135w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/10\/Frame-3.png?resize=768%2C1701&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/10\/Frame-3.png?resize=694%2C1536&amp;ssl=1 694w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/10\/Frame-3.png?resize=925%2C2048&amp;ssl=1 925w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/10\/Frame-3.png?w=1184&amp;ssl=1 1184w\" sizes=\"auto, (max-width: 462px) 100vw, 462px\" \/><\/figure>\n<\/div>\n\n\n<p>But I was immediately confused when thinking about how to do this with scroll-driven animations. <strong>The problem is that that &#8220;section&#8221; itself is the thing we need to apply the <code>animation-timeline: view();<\/code> to, such that we have the proper moment to react to (&#8220;<em>the section is currently in view!<\/em>&#8220;).<\/strong> But in my diagram above, it&#8217;s actually a <code>&lt;blockquote&gt;<\/code> that we need to apply special conditional styling to, not the section. In a <code>@keyframe<\/code> animation, all we can do is change declarations, we can&#8217;t select other elements. Apologies if that confusing, but the root of is that we need to transfer styles from the section to the blockquote without using selectors \u2014 and it&#8217;s weird.<\/p>\n\n\n\n<p>The good news is that what we can do is update CSS custom properties on the section, and those values will cascade to all the children of the section, and we can use those to style the blockquote.<\/p>\n\n\n\n<p>First, in order to make a custom property animatable, we need to declare it&#8217;s type. Let&#8217;s do a fade in first, thus we need opacity:<\/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-keyword\">@property<\/span> --blockquoteOpacity {\n  <span class=\"hljs-selector-tag\">syntax<\/span>: \"&lt;<span class=\"hljs-selector-tag\">percentage<\/span>&gt;\";\n  <span class=\"hljs-selector-tag\">inherits<\/span>: <span class=\"hljs-selector-tag\">true<\/span>;\n  <span class=\"hljs-selector-tag\">initial-value<\/span>: 0%;\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>Now the section itself has the animation timeline:<\/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\">section<\/span><span class=\"hljs-selector-class\">.has-pullquote<\/span> {\n  <span class=\"hljs-attribute\">animation<\/span>: reveal linear both;\n  <span class=\"hljs-attribute\">animation-timeline<\/span>: <span class=\"hljs-built_in\">view<\/span>();\n  <span class=\"hljs-attribute\">animation-range<\/span>: cover <span class=\"hljs-number\">0%<\/span> cover <span class=\"hljs-number\">100%<\/span>;\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>And that animation we&#8217;ve named <code>reveal<\/code> above can now update the custom property:<\/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\">@keyframes<\/span> reveal {\n  <span class=\"hljs-selector-tag\">from<\/span> {\n    <span class=\"hljs-attribute\">--blockquoteOpacity<\/span>: <span class=\"hljs-number\">0%<\/span>;\n  }\n  <span class=\"hljs-selector-tag\">to<\/span>% {\n    <span class=\"hljs-attribute\">--blockquoteOpacity<\/span>: <span class=\"hljs-number\">100%<\/span>;\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>Now as the animation runs, based on it&#8217;s visibility in the viewport, it will update the custom property and thus fade\/in out the blockquote:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">blockquote<\/span> {\n  <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-built_in\">var<\/span>(--blockquoteOpacity);\n\n  <span class=\"hljs-attribute\">position<\/span>: sticky;\n  <span class=\"hljs-attribute\">top<\/span>: <span class=\"hljs-number\">50%<\/span>;\n  <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">translateY<\/span>(-<span class=\"hljs-number\">50%<\/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\">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>Note I&#8217;m using <code>position: sticky<\/code> in there too, which will keep our blockquote in the middle of the viewport while we&#8217;re cruising through that section. <\/p>\n\n\n\n<p>Try it out (Chrome &#8216;n&#8217; friends have stable browser support):<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_gOVXVjj\" src=\"\/\/codepen.io\/anon\/embed\/gOVXVjj?height=650&amp;theme-id=47434&amp;slug-hash=gOVXVjj&amp;default-tab=result\" height=\"650\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed gOVXVjj\" title=\"CodePen Embed gOVXVjj\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Here&#8217;s a video of it working in case you&#8217;re in a non-supporting browser:<\/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='375' src='https:\/\/videopress.com\/embed\/1ia2HgER?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=1725245713'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>Because we instantiated the opacity custom property for the opacity at 100%, even in a non-supporting browser like Safari, the blockquote will be visible and it&#8217;s a fine experience. <\/p>\n\n\n\n<p>I found this all a little fiddly, but I&#8217;m not even sure I&#8217;m doing this &#8220;correctly&#8221;. Maybe there is a way to tap into another elements view timeline I&#8217;m not aware of? If I&#8217;m doing it the intended way, I could see this getting pretty cumbersome with lots of elements and lots of different values needing updated. But after all, that&#8217;s the job sometimes. This is intricate stuff and we&#8217;re using the CSS primitives directly. The control we have is quite fine-grained, and that&#8217;s a good thing! <\/p>\n\n\n<div class=\"box article-series\">\n  <header>\n    <h3 class=\"article-series-header\">Article Series<\/h3>\n  <\/header>\n  <div class=\"box-content\">\n            <ol>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/scroll-driven-sections\/\">Scroll-Driven&#8230; Sections<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/named-scroll-view-timelines\/\">Named Scroll &amp; View Timelines<\/a>\n            <\/li>\n                      <li>\n              <a href=\"https:\/\/frontendmasters.com\/blog\/scoped-scroll-timelines\/\">(Up-) Scoped Scroll Timelines<\/a>\n            <\/li>\n                  <\/ol>\n        <\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>If you&#8217;re creating a scroll-driven animation and the goal is &#8220;when the page is scrolling through this general section, animate the children&#8221; it&#8217;s probably going to involve passing scrolling data through custom properties. <\/p>\n","protected":false},"author":1,"featured_media":4281,"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":[7,57],"class_list":["post-4277","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-css","tag-scroll-driven-animations"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/10\/Screenshot-2024-10-29-at-8.35.22%E2%80%AFAM.png?fit=1000%2C604&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4277","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=4277"}],"version-history":[{"count":3,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4277\/revisions"}],"predecessor-version":[{"id":4574,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4277\/revisions\/4574"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/4281"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=4277"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=4277"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=4277"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}