[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-scroll-driven-animations":32,"code:js:true:lnlifp":265,"code:css:true:6otd0o":266,"code:css:true:rykvnd":267,"code:js:true:1gygeim":268,"code:css:true:1stf5v7":269,"code:css:true:crm7xe":270,"code:css:true:1gx3df0":271},[4,20],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Build. Learn. Ship.","Practical courses, engineering-grade articles and open-source tools for people who ship.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Structured paths, listening drills and spaced repetition that actually sticks.","amber-400","⌘",[13,15,29],"es",{"courses":17,"paths":17,"articles":18,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":17},2,{"id":33,"slug":34,"title":35,"subtitle":36,"excerpt":37,"coverUrl":38,"locale":13,"readingMinutes":39,"publishedAt":40,"viewCount":41,"likeCount":19,"commentCount":19,"author":42,"vertical":47,"topic":48,"tags":51,"_count":63,"playground":65,"body":67,"bodyMd":248,"seo":249,"translationGroupId":252,"thread":253,"assessments":255,"translations":261,"quiz":263},"019fe65f-ef22-765a-97c7-cdfdbb168024","scroll-driven-animations","Your scroll listener is doing CSS's job",null,"Reading progress bars, reveal-on-scroll effects, parallax — you've been writing these in JavaScript. CSS scroll-driven animations bind them directly to scroll position, off the main thread, in a handful of lines.","\u002Fmedia\u002Fcovers\u002Fscroll-driven-animations.png",5,"2026-07-06T10:47:02.014Z",16,{"id":43,"name":44,"username":45,"avatarUrl":36,"headline":46},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":49,"name":50},"css","CSS",[52,54,57,60],{"slug":49,"name":53,"color":36},"Css",{"slug":55,"name":56,"color":36},"webdev","Webdev",{"slug":58,"name":59,"color":36},"frontend","Frontend",{"slug":61,"name":62,"color":36},"javascript","Javascript",{"assessments":64},1,{"slug":34,"title":66},"CSS scroll-driven animations",{"blocks":68,"version":64},[69,73,79,82,85,88,92,95,100,103,106,110,113,117,120,123,127,130,134,137,141,144,147,151,154,157,160,163,166,169,172,175,178,181,184,187,190,193,196,200,203,206,209,212,215,218,221,224,227,230,233,236,239],{"id":70,"html":71,"type":72},"b1","\u003Cp>You&#39;ve written this, or something very close to it:\u003C\u002Fp>","paragraph",{"id":74,"code":75,"type":76,"language":77,"highlight":78},"b2","window.addEventListener('scroll', () => {\n  const scrolled   = window.scrollY;\n  const total      = document.documentElement.scrollHeight - window.innerHeight;\n  const progress   = (scrolled \u002F total) * 100;\n  progressBar.style.width = `${progress}%`;\n});","code","js",[],{"id":80,"html":81,"type":72},"b3","\u003Cp>Three lines of arithmetic, firing on every scroll event. On a 120 Hz display, that callback runs 120 times a second. If anything else is competing for the main thread at that moment — a re-render, a layout calculation, a lazy-loaded image — the bar stutters.\u003C\u002Fp>",{"id":83,"html":84,"type":72},"b4","\u003Cp>All that effort, and the whole thing is animating a \u003Ccode>div\u003C\u002Fcode> with a \u003Ccode>background-color\u003C\u002Fcode>.\u003C\u002Fp>",{"id":86,"html":87,"type":72},"b5","\u003Cp>Here&#39;s the same effect in CSS:\u003C\u002Fp>",{"id":89,"code":90,"type":76,"language":49,"highlight":91},"b6","@keyframes grow-progress {\n  from { transform: scaleX(0); }\n  to   { transform: scaleX(1); }\n}\n\n#progress-bar {\n  transform-origin: left;\n  animation: grow-progress linear;\n  animation-timeline: scroll();\n}",[],{"id":93,"html":94,"type":72},"b7","\u003Cp>No event listener. No math. No \u003Ccode>requestAnimationFrame\u003C\u002Fcode>. The browser wires the animation to the scroll position directly — and because it runs on the compositor thread, it doesn&#39;t compete with JavaScript at all.\u003C\u002Fp>",{"id":96,"html":97,"text":98,"type":99,"level":31},"b8","What \u003Ccode>scroll()\u003C\u002Fcode> actually is","What scroll() actually is","heading",{"id":101,"html":102,"type":72},"b9","\u003Cp>\u003Ccode>animation-timeline: scroll()\u003C\u002Fcode> creates a timeline whose progress maps to the scroll position of the nearest scrollable ancestor — by default, the root \u003Ccode>&lt;html&gt;\u003C\u002Fcode> element. When you&#39;re at the top, the timeline is at 0%. At the bottom, it&#39;s at 100%. In between, it&#39;s linear. The \u003Ccode>@keyframes\u003C\u002Fcode> animation plays against that timeline exactly as if you&#39;d written the numbers yourself.\u003C\u002Fp>",{"id":104,"html":105,"type":72},"b10","\u003Cp>You can control the target scrollable and the axis:\u003C\u002Fp>",{"id":107,"code":108,"type":76,"language":49,"highlight":109},"b11","\u002F* default — root, vertical (block) axis *\u002F\nanimation-timeline: scroll();\n\n\u002F* the element's own scroll container *\u002F\nanimation-timeline: scroll(self);\n\n\u002F* horizontal scroll *\u002F\nanimation-timeline: scroll(inline);",[],{"id":111,"html":112,"type":72},"b12","\u003Cp>The reading-progress bar is the obvious case. Parallax effects are the other one: a background image that moves at a different speed than the page. What used to require \u003Ccode>scroll\u003C\u002Fcode> listeners and \u003Ccode>translate\u003C\u002Fcode> math is now two CSS properties pointing at the same timeline.\u003C\u002Fp>",{"id":114,"html":115,"text":116,"type":99,"level":31},"b13","\u003Ccode>view()\u003C\u002Fcode> — the IntersectionObserver you never wanted to write","view() — the IntersectionObserver you never wanted to write",{"id":118,"html":119,"type":72},"b14","\u003Cp>\u003Ccode>scroll()\u003C\u002Fcode> is driven by the document-level scroll position. \u003Ccode>view()\u003C\u002Fcode> is driven by where a \u003Cem>specific element\u003C\u002Fem> sits relative to the viewport — it plays as the element enters and exits the scrollport.\u003C\u002Fp>",{"id":121,"html":122,"type":72},"b15","\u003Cp>This is the pattern you&#39;ve reached for IntersectionObserver to fake:\u003C\u002Fp>",{"id":124,"code":125,"type":76,"language":77,"highlight":126},"b16","\u002F\u002F The old way\nconst observer = new IntersectionObserver(entries => {\n  entries.forEach(entry => {\n    if (entry.isIntersecting) {\n      entry.target.classList.add('visible');\n    }\n  });\n}, { threshold: 0.1 });\n\ndocument.querySelectorAll('.card').forEach(card => observer.observe(card));",[],{"id":128,"html":129,"type":72},"b17","\u003Cp>Plus the companion CSS that lives in \u003Ccode>.card.visible\u003C\u002Fcode>. Here&#39;s the same effect without any JavaScript:\u003C\u002Fp>",{"id":131,"code":132,"type":76,"language":49,"highlight":133},"b18","@keyframes reveal {\n  from { opacity: 0; transform: translateY(24px); }\n  to   { opacity: 1; transform: translateY(0); }\n}\n\n.card {\n  animation: reveal linear forwards;\n  animation-timeline: view();\n  animation-range: entry 0% entry 100%;\n}",[],{"id":135,"html":136,"type":72},"b19","\u003Cp>Every \u003Ccode>.card\u003C\u002Fcode> fades and lifts in as it enters the viewport. One rule, zero observers, zero \u003Ccode>classList\u003C\u002Fcode> mutations.\u003C\u002Fp>",{"id":138,"html":139,"text":140,"type":99,"level":31},"b20","Controlling the timing with \u003Ccode>animation-range\u003C\u002Fcode>","Controlling the timing with animation-range",{"id":142,"html":143,"type":72},"b21","\u003Cp>Without \u003Ccode>animation-range\u003C\u002Fcode>, \u003Ccode>view()\u003C\u002Fcode> plays across the entire time an element is \u003Cem>anywhere\u003C\u002Fem> in the viewport — entry, full screen, exit — which usually isn&#39;t what you want. \u003Ccode>animation-range\u003C\u002Fcode> pins the animation to a specific phase.\u003C\u002Fp>",{"id":145,"html":146,"type":72},"b22","\u003Cp>The phases map to where the element is relative to the scrollport boundary:\u003C\u002Fp>",{"id":148,"code":149,"type":76,"language":49,"highlight":150},"b23","\u002F* Animate only while the element is entering *\u002F\nanimation-range: entry 0% entry 100%;\n\n\u002F* Animate while the element crosses the center of the viewport *\u002F\nanimation-range: contain 0% contain 100%;\n\n\u002F* Animate while the element is leaving *\u002F\nanimation-range: exit 0% exit 100%;",[],{"id":152,"html":153,"type":72},"b24","\u003Cp>\u003Ccode>entry 0%\u003C\u002Fcode> is the moment the element&#39;s leading edge first touches the viewport. \u003Ccode>entry 100%\u003C\u002Fcode> is when it&#39;s fully inside. Use \u003Ccode>forwards\u003C\u002Fcode> fill mode to hold the final state — otherwise the element snaps back when the range ends.\u003C\u002Fp>",{"id":155,"html":156,"type":72},"b25","\u003Cp>Quick prediction: if you set \u003Ccode>animation-range: entry 0% exit 100%\u003C\u002Fcode>, what does the animation do as the user scrolls the element back off the top of the screen?\u003C\u002Fp>",{"id":158,"html":159,"type":72},"b26","\u003Cp>It reverses — the element fades back out as it exits. Scroll-driven animations play in both directions by default, tracking position, not time. Add \u003Ccode>animation-fill-mode: forwards\u003C\u002Fcode> if you want it to hold.\u003C\u002Fp>",{"id":161,"html":162,"type":72},"b27","\u003C!-- playground:start -->",{"id":164,"html":165,"text":165,"type":99,"level":31},"b28","🎮 Try it yourself",{"id":167,"html":168,"type":72},"b29","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fposts\u002F2026-07-06-scroll-driven-animations\u002Fplayground\u002F\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":170,"html":171,"type":72},"b30","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":173,"html":174,"type":72},"b31","\u003C!-- playground:end -->",{"id":176,"html":177,"text":177,"type":99,"level":31},"b32","Where JavaScript still belongs",{"id":179,"html":180,"type":72},"b33","\u003Cp>Scroll-driven animations aren&#39;t a full IntersectionObserver replacement. The distinction is about what kind of response you need.\u003C\u002Fp>",{"id":182,"html":183,"type":72},"b34","\u003Cp>Reach for CSS when you need \u003Cstrong>continuous, position-linked motion\u003C\u002Fstrong>: progress bars, parallax, reveal effects, sticky header opacity fading in as you scroll past a threshold. The animation tracks the scroll position in real time.\u003C\u002Fp>",{"id":185,"html":186,"type":72},"b35","\u003Cp>Keep JavaScript when you need \u003Cstrong>one-shot logic on threshold\u003C\u002Fstrong>: lazy loading an image when it enters view, firing an analytics event, adding a class that won&#39;t reverse. IntersectionObserver is a better model for &quot;something happened once at this scroll depth.&quot;\u003C\u002Fp>",{"id":188,"html":189,"type":72},"b36","\u003Cp>The smell that tells you you&#39;re in the wrong tool: you&#39;re writing a scroll event listener that does math, then sets a style property. If the output is always a CSS value derived from a scroll position, CSS can own it.\u003C\u002Fp>",{"id":191,"html":192,"text":192,"type":99,"level":31},"b37","Browser support — the honest picture",{"id":194,"html":195,"type":72},"b38","\u003Cp>Chrome 115 (mid-2023) shipped scroll-driven animations. Firefox followed in Firefox 128 (mid-2024). Safari support arrived in Safari 18 (late 2024). As of mid-2025, you&#39;re looking at good baseline coverage, but still worth a \u003Ccode>@supports\u003C\u002Fcode> guard on anything production-critical:\u003C\u002Fp>",{"id":197,"code":198,"type":76,"language":49,"highlight":199},"b39","@supports (animation-timeline: scroll()) {\n  #progress-bar {\n    animation: grow-progress linear;\n    animation-timeline: scroll();\n  }\n}",[],{"id":201,"html":202,"type":72},"b40","\u003Cp>Browsers without support silently skip the block. The content is still there; the enhancement just doesn&#39;t show up. That&#39;s the exact progressive-enhancement story view transitions and container queries tell — pick it up where supported, ignore it where not.\u003C\u002Fp>",{"id":204,"html":205,"type":72},"b41","\u003C!-- quiz:start -->",{"id":207,"html":208,"text":208,"type":99,"level":31},"b42","🧠 Test yourself",{"id":210,"html":211,"type":72},"b43","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fquiz\u002Ftake.html?post=2026-07-06-scroll-driven-animations\">Take the 6-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":213,"html":214,"type":72},"b44","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":216,"html":217,"type":72},"b45","\u003C!-- quiz:end -->",{"id":219,"html":220,"text":220,"type":99,"level":31},"b46","Stop writing scroll listeners for CSS problems",{"id":222,"html":223,"type":72},"b47","\u003Cp>That event listener has been load-bearing for so long it&#39;s easy to forget it was always a workaround. The scroll position was always data the browser had. The motion was always a style property. The glue between them — the event, the math, the \u003Ccode>style.width =\u003C\u002Fcode> — was the gap the platform hadn&#39;t closed yet.\u003C\u002Fp>",{"id":225,"html":226,"type":72},"b48","\u003Cp>\u003Ccode>animation-timeline\u003C\u002Fcode> closes the gap. The scroll listener you&#39;re writing today for a progress bar or a reveal effect is doing CSS&#39;s job, on your thread, with your budget.\u003C\u002Fp>",{"id":228,"html":229,"type":72},"b49","\u003Cp>\u003Cstrong>The rule for next time:\u003C\u002Fstrong> reach for a scroll event listener when you need to \u003Cem>react to scroll with logic\u003C\u002Fem>. Reach for \u003Ccode>animation-timeline\u003C\u002Fcode> when you need to react with \u003Cem>motion\u003C\u002Fem>. Most reveal effects and progress indicators are motion, and they always were.\u003C\u002Fp>",{"id":231,"html":232,"type":72},"b50","\u003Cp>What scroll effect are you currently holding together with a 60-fps event listener — and how small does the CSS version turn out to be?\u003C\u002Fp>",{"id":234,"type":235},"b51","divider",{"id":237,"html":238,"type":72},"b52","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":240,"type":241,"items":242,"ordered":18},"b53","list",[243,244,245,246,247],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","💼 \u003Cstrong>LinkedIn\u003C\u002Fstrong> — \u003Ca href=\"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F\">linkedin.com\u002Fin\u002Fparsa-jiravand\u003C\u002Fa>","✉️ \u003Cstrong>Email\u003C\u002Fstrong> (work &amp; contract inquiries): \u003Ca href=\"mailto:bestpractice2026@gmail.com\">bestpractice2026@gmail.com\u003C\u002Fa>","You've written this, or something very close to it:\n\n```js\nwindow.addEventListener('scroll', () => {\n  const scrolled   = window.scrollY;\n  const total      = document.documentElement.scrollHeight - window.innerHeight;\n  const progress   = (scrolled \u002F total) * 100;\n  progressBar.style.width = `${progress}%`;\n});\n```\n\nThree lines of arithmetic, firing on every scroll event. On a 120 Hz display, that callback runs 120 times a second. If anything else is competing for the main thread at that moment — a re-render, a layout calculation, a lazy-loaded image — the bar stutters.\n\nAll that effort, and the whole thing is animating a `div` with a `background-color`.\n\nHere's the same effect in CSS:\n\n```css\n@keyframes grow-progress {\n  from { transform: scaleX(0); }\n  to   { transform: scaleX(1); }\n}\n\n#progress-bar {\n  transform-origin: left;\n  animation: grow-progress linear;\n  animation-timeline: scroll();\n}\n```\n\nNo event listener. No math. No `requestAnimationFrame`. The browser wires the animation to the scroll position directly — and because it runs on the compositor thread, it doesn't compete with JavaScript at all.\n\n## What `scroll()` actually is\n\n`animation-timeline: scroll()` creates a timeline whose progress maps to the scroll position of the nearest scrollable ancestor — by default, the root `\u003Chtml>` element. When you're at the top, the timeline is at 0%. At the bottom, it's at 100%. In between, it's linear. The `@keyframes` animation plays against that timeline exactly as if you'd written the numbers yourself.\n\nYou can control the target scrollable and the axis:\n\n```css\n\u002F* default — root, vertical (block) axis *\u002F\nanimation-timeline: scroll();\n\n\u002F* the element's own scroll container *\u002F\nanimation-timeline: scroll(self);\n\n\u002F* horizontal scroll *\u002F\nanimation-timeline: scroll(inline);\n```\n\nThe reading-progress bar is the obvious case. Parallax effects are the other one: a background image that moves at a different speed than the page. What used to require `scroll` listeners and `translate` math is now two CSS properties pointing at the same timeline.\n\n## `view()` — the IntersectionObserver you never wanted to write\n\n`scroll()` is driven by the document-level scroll position. `view()` is driven by where a *specific element* sits relative to the viewport — it plays as the element enters and exits the scrollport.\n\nThis is the pattern you've reached for IntersectionObserver to fake:\n\n```js\n\u002F\u002F The old way\nconst observer = new IntersectionObserver(entries => {\n  entries.forEach(entry => {\n    if (entry.isIntersecting) {\n      entry.target.classList.add('visible');\n    }\n  });\n}, { threshold: 0.1 });\n\ndocument.querySelectorAll('.card').forEach(card => observer.observe(card));\n```\n\nPlus the companion CSS that lives in `.card.visible`. Here's the same effect without any JavaScript:\n\n```css\n@keyframes reveal {\n  from { opacity: 0; transform: translateY(24px); }\n  to   { opacity: 1; transform: translateY(0); }\n}\n\n.card {\n  animation: reveal linear forwards;\n  animation-timeline: view();\n  animation-range: entry 0% entry 100%;\n}\n```\n\nEvery `.card` fades and lifts in as it enters the viewport. One rule, zero observers, zero `classList` mutations.\n\n## Controlling the timing with `animation-range`\n\nWithout `animation-range`, `view()` plays across the entire time an element is *anywhere* in the viewport — entry, full screen, exit — which usually isn't what you want. `animation-range` pins the animation to a specific phase.\n\nThe phases map to where the element is relative to the scrollport boundary:\n\n```css\n\u002F* Animate only while the element is entering *\u002F\nanimation-range: entry 0% entry 100%;\n\n\u002F* Animate while the element crosses the center of the viewport *\u002F\nanimation-range: contain 0% contain 100%;\n\n\u002F* Animate while the element is leaving *\u002F\nanimation-range: exit 0% exit 100%;\n```\n\n`entry 0%` is the moment the element's leading edge first touches the viewport. `entry 100%` is when it's fully inside. Use `forwards` fill mode to hold the final state — otherwise the element snaps back when the range ends.\n\nQuick prediction: if you set `animation-range: entry 0% exit 100%`, what does the animation do as the user scrolls the element back off the top of the screen?\n\nIt reverses — the element fades back out as it exits. Scroll-driven animations play in both directions by default, tracking position, not time. Add `animation-fill-mode: forwards` if you want it to hold.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fposts\u002F2026-07-06-scroll-driven-animations\u002Fplayground\u002F)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Where JavaScript still belongs\n\nScroll-driven animations aren't a full IntersectionObserver replacement. The distinction is about what kind of response you need.\n\nReach for CSS when you need **continuous, position-linked motion**: progress bars, parallax, reveal effects, sticky header opacity fading in as you scroll past a threshold. The animation tracks the scroll position in real time.\n\nKeep JavaScript when you need **one-shot logic on threshold**: lazy loading an image when it enters view, firing an analytics event, adding a class that won't reverse. IntersectionObserver is a better model for \"something happened once at this scroll depth.\"\n\nThe smell that tells you you're in the wrong tool: you're writing a scroll event listener that does math, then sets a style property. If the output is always a CSS value derived from a scroll position, CSS can own it.\n\n## Browser support — the honest picture\n\nChrome 115 (mid-2023) shipped scroll-driven animations. Firefox followed in Firefox 128 (mid-2024). Safari support arrived in Safari 18 (late 2024). As of mid-2025, you're looking at good baseline coverage, but still worth a `@supports` guard on anything production-critical:\n\n```css\n@supports (animation-timeline: scroll()) {\n  #progress-bar {\n    animation: grow-progress linear;\n    animation-timeline: scroll();\n  }\n}\n```\n\nBrowsers without support silently skip the block. The content is still there; the enhancement just doesn't show up. That's the exact progressive-enhancement story view transitions and container queries tell — pick it up where supported, ignore it where not.\n\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 6-question quiz →](https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fquiz\u002Ftake.html?post=2026-07-06-scroll-driven-animations)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n## Stop writing scroll listeners for CSS problems\n\nThat event listener has been load-bearing for so long it's easy to forget it was always a workaround. The scroll position was always data the browser had. The motion was always a style property. The glue between them — the event, the math, the `style.width =` — was the gap the platform hadn't closed yet.\n\n`animation-timeline` closes the gap. The scroll listener you're writing today for a progress bar or a reveal effect is doing CSS's job, on your thread, with your budget.\n\n**The rule for next time:** reach for a scroll event listener when you need to *react to scroll with logic*. Reach for `animation-timeline` when you need to react with *motion*. Most reveal effects and progress indicators are motion, and they always were.\n\nWhat scroll effect are you currently holding together with a 60-fps event listener — and how small does the CSS version turn out to be?\n\n---\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)\n- 💼 **LinkedIn** — [linkedin.com\u002Fin\u002Fparsa-jiravand](https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F)\n- ✉️ **Email** (work & contract inquiries): [bestpractice2026@gmail.com](mailto:bestpractice2026@gmail.com)",{"title":35,"canonical":250,"description":251},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fscroll-driven-animations","Reading progress bars, reveal-on-scroll effects, parallax — you've been writing these in JavaScript. CSS scroll-driven animations bind them directly to scroll position, off the mai","019fe65f-ef23-7399-b4c3-a720cc942cd7",{"id":254,"locked":18},"019fe65f-f5e9-71ba-98b1-d76956a85b36",[256],{"id":257,"slug":34,"title":258,"_count":259},"019fe776-24fa-7629-a158-1b7b4f49e67a","Scroll-driven animations",{"questions":260},6,[262],{"locale":13,"slug":34},{"id":257,"slug":34,"title":258,"_count":264,"questionCount":260},{"questions":260},"\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">window\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'scroll'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> scrolled   \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> window\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>scrollY;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> total      \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> document\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>documentElement\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>scrollHeight \u003Cspan class=\"shj-oper\">-\u003C\u002Fspan> window\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>innerHeight;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> progress   \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>scrolled \u003Cspan class=\"shj-oper\">\u002F\u003C\u002Fspan> total\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">*\u003C\u002Fspan> \u003Cspan class=\"shj-num\">100\u003C\u002Fspan>;\n  progressBar\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>style\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>width \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-str\">`\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>progress\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">%`\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">@keyframes\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">grow-progress\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-var\">transform\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">scaleX\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">to\u003C\u002Fspan>   \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-var\">transform\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">scaleX\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-func\">#progress-bar\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">transform-origin\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">left\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">animation\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">grow-progress\u003C\u002Fspan> \u003Cspan class=\"shj-num\">linear\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">animation-timeline\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">scroll\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F* default — root, vertical (block) axis *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">animation-timeline\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">scroll\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-cmnt\">\u002F* the element's own scroll container *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">animation-timeline\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">scroll\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">self\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-cmnt\">\u002F* horizontal scroll *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">animation-timeline\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">scroll\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">inline\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F The old way\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> observer \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">IntersectionObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entries \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  entries\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">forEach\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>isIntersecting\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>target\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>classList\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">add\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'visible'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> threshold\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0.1\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\ndocument\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">querySelectorAll\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'.card'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">forEach\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>card \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> observer\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">observe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>card\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">@keyframes\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">reveal\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-var\">opacity\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>; \u003Cspan class=\"shj-var\">transform\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">translateY\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">24\u003C\u002Fspan>\u003Cspan class=\"shj-var\">px\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">to\u003C\u002Fspan>   \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-var\">opacity\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>; \u003Cspan class=\"shj-var\">transform\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">translateY\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-var\">.card\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">animation\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">reveal\u003C\u002Fspan> \u003Cspan class=\"shj-num\">linear\u003C\u002Fspan> \u003Cspan class=\"shj-num\">forwards\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">animation-timeline\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">view\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">animation-range\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">entry\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-var\">%\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan> \u003Cspan class=\"shj-num\">entry\u003C\u002Fspan> \u003Cspan class=\"shj-num\">100\u003C\u002Fspan>\u003Cspan class=\"shj-var\">%\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F* Animate only while the element is entering *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">animation-range\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">entry\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-var\">%\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan> \u003Cspan class=\"shj-num\">entry\u003C\u002Fspan> \u003Cspan class=\"shj-num\">100\u003C\u002Fspan>\u003Cspan class=\"shj-var\">%\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-cmnt\">\u002F* Animate while the element crosses the center of the viewport *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">animation-range\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">contain\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-var\">%\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan> \u003Cspan class=\"shj-num\">contain\u003C\u002Fspan> \u003Cspan class=\"shj-num\">100\u003C\u002Fspan>\u003Cspan class=\"shj-var\">%\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-cmnt\">\u002F* Animate while the element is leaving *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">animation-range\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">exit\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-var\">%\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan> \u003Cspan class=\"shj-num\">exit\u003C\u002Fspan> \u003Cspan class=\"shj-num\">100\u003C\u002Fspan>\u003Cspan class=\"shj-var\">%\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">@supports\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">animation-timeline\u003C\u002Fspan>: \u003Cspan class=\"shj-kwd\">scroll\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-func\">#progress-bar\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">animation\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">grow-progress\u003C\u002Fspan> \u003Cspan class=\"shj-num\">linear\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-var\">animation-timeline\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">scroll\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>"]