[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-scroll-driven-animations":32},[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,{"slug":33,"title":34,"summary":35,"html":36,"css":37,"js":38,"config":39,"doc":41},"scroll-driven-animations","CSS scroll-driven animations",null,"\u003Cdiv class=\"wrap\">\n    \u003Ch1>🎞️ Scroll-driven animations — no listeners\u003C\u002Fh1>\n    \u003Cp class=\"sub\">\n      Scroll the panel below. The top bar is bound to scroll with\n      \u003Ccode>animation-timeline: scroll()\u003C\u002Fcode>; the cards reveal with\n      \u003Ccode>view()\u003C\u002Fcode>. Zero JavaScript drives the motion. Change the\n      \u003Cb>range\u003C\u002Fb> to feel each phase — including \u003Cb>in &amp; out\u003C\u002Fb>, which\n      reverses as a card leaves the top.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"demo\">\n      \u003Cdiv class=\"controls\" role=\"group\" aria-label=\"animation-range preset\">\n        \u003Cspan class=\"lbl\">range\u003C\u002Fspan>\n        \u003Cbutton data-range=\"entry\"   aria-pressed=\"true\">reveal in\u003C\u002Fbutton>\n        \u003Cbutton data-range=\"contain\" aria-pressed=\"false\">while crossing\u003C\u002Fbutton>\n        \u003Cbutton data-range=\"exit\"    aria-pressed=\"false\">fade on exit\u003C\u002Fbutton>\n        \u003Cbutton data-range=\"inout\"   aria-pressed=\"false\">in &amp; out\u003C\u002Fbutton>\n        \u003Cbutton id=\"top\" style=\"margin-left:auto\">↑ reset\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n\n      \u003Ccode class=\"chip\" id=\"css\">animation-range: entry 0% entry 100%;\u003C\u002Fcode>\n\n      \u003Cdiv class=\"bar\" aria-hidden=\"true\">\u003Ci>\u003C\u002Fi>\u003C\u002Fdiv>\n\n      \u003Cdiv class=\"scroller\" id=\"scroller\" tabindex=\"0\" aria-label=\"scrollable demo panel\">\n        \u003Cp class=\"hint\">↓ scroll me ↓\u003C\u002Fp>\n        \u003Cdiv class=\"cards r-entry\" id=\"cards\">\n          \u003Cdiv class=\"card\">\u003Cspan class=\"n\">1\u003C\u002Fspan>\u003Cspan>\u003Cb>Bound to scroll position\u003C\u002Fb>\u003Csmall>not time — scroll up and it plays backward\u003C\u002Fsmall>\u003C\u002Fspan>\u003C\u002Fdiv>\n          \u003Cdiv class=\"card\">\u003Cspan class=\"n\">2\u003C\u002Fspan>\u003Cspan>\u003Cb>view() = IntersectionObserver\u003C\u002Fb>\u003Csmall>…that you never had to write\u003C\u002Fsmall>\u003C\u002Fspan>\u003C\u002Fdiv>\n          \u003Cdiv class=\"card\">\u003Cspan class=\"n\">3\u003C\u002Fspan>\u003Cspan>\u003Cb>Runs off the main thread\u003C\u002Fb>\u003Csmall>on the compositor — no jank\u003C\u002Fsmall>\u003C\u002Fspan>\u003C\u002Fdiv>\n          \u003Cdiv class=\"card\">\u003Cspan class=\"n\">4\u003C\u002Fspan>\u003Cspan>\u003Cb>animation-range picks the phase\u003C\u002Fb>\u003Csmall>entry · contain · exit\u003C\u002Fsmall>\u003C\u002Fspan>\u003C\u002Fdiv>\n          \u003Cdiv class=\"card\">\u003Cspan class=\"n\">5\u003C\u002Fspan>\u003Cspan>\u003Cb>One CSS rule\u003C\u002Fb>\u003Csmall>no observers, no classList toggles\u003C\u002Fsmall>\u003C\u002Fspan>\u003C\u002Fdiv>\n          \u003Cdiv class=\"card\">\u003Cspan class=\"n\">6\u003C\u002Fspan>\u003Cspan>\u003Cb>Progressive enhancement\u003C\u002Fb>\u003Csmall>@supports guards it; old browsers just skip\u003C\u002Fsmall>\u003C\u002Fspan>\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n        \u003Cp class=\"end\">Now scroll back up — with \u003Cb>in &amp; out\u003C\u002Fb> the cards fade away as they exit.\u003C\u002Fp>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"nosupport\">\n      Your browser doesn't support \u003Ccode>animation-timeline\u003C\u002Fcode> yet — the whole point of the post!\n      Try the latest Chrome or Firefox, or Safari 18+, to see the demo.\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>",":root {\n    --bg: #0f1115; --panel: #171a21; --sunk: #0b0d12; --ink: #e8ebf2; --muted: #99a1b3;\n    --accent: #6ea8fe; --accent2: #c792ea; --good: #3fb950; --line: #262b36;\n    font-family: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n  }\n  * { box-sizing: border-box; }\n  body { margin: 0; background: var(--bg); color: var(--ink); min-height: 100vh;\n    display: flex; align-items: center; justify-content: center; padding: 18px; }\n  .wrap { width: 100%; max-width: 620px; timeline-scope: --panel; }\n  h1 { font-size: 1.15rem; margin: 0 0 2px; }\n  .sub { color: var(--muted); font-size: .88rem; margin: 0 0 16px; line-height: 1.45; }\n  .sub code, code.chip { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n\n  \u002F* --- controls --- *\u002F\n  .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }\n  .lbl { color: var(--muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .5px; margin-right: 2px; }\n  .controls button {\n    font: inherit; font-size: .85rem; font-weight: 600; color: var(--ink);\n    background: var(--panel); border: 1px solid var(--line); border-radius: 999px;\n    padding: 7px 13px; cursor: pointer; transition: background .15s, border-color .15s, color .15s;\n  }\n  .controls button[aria-pressed=\"true\"] { background: var(--accent); color: #0b0d10; border-color: var(--accent); }\n  .controls button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n  code.chip { display: inline-block; font-size: .8rem; color: var(--accent2);\n    background: var(--sunk); border: 1px solid var(--line); border-radius: 8px; padding: 5px 9px; }\n\n  \u002F* --- scroll() progress bar (pure CSS, bound to the panel's scroll) --- *\u002F\n  .bar { height: 8px; background: var(--line); border-radius: 999px; overflow: hidden; margin-bottom: 10px; }\n  .bar > i { display: block; height: 100%; border-radius: 999px;\n    background: linear-gradient(90deg, var(--accent), var(--accent2));\n    transform: scaleX(0); transform-origin: left;\n    animation: grow linear both; animation-timeline: --panel; }\n  @keyframes grow { to { transform: scaleX(1); } }\n\n  \u002F* --- the scroll panel + its own scroll timeline --- *\u002F\n  .scroller {\n    height: 340px; overflow-y: auto; scroll-behavior: smooth; position: relative;\n    background: var(--sunk); border: 1px solid var(--line); border-radius: 12px; padding: 14px;\n    scroll-timeline: --panel y;\n  }\n  .hint, .end { text-align: center; color: var(--muted); font-size: .8rem; margin: 6px 0 14px; }\n  .end { margin: 16px 0 6px; }\n\n  \u002F* --- view() reveal cards --- *\u002F\n  .cards { display: grid; gap: 14px; }\n  .card {\n    background: var(--panel); border: 1px solid var(--line); border-radius: 12px;\n    padding: 16px 18px; display: flex; align-items: center; gap: 14px;\n    animation: reveal linear both; animation-timeline: view();\n    animation-range: entry 0% entry 100%;\n  }\n  .card .n { font-size: 1.3rem; font-weight: 800; color: var(--accent);\n    background: var(--sunk); border: 1px solid var(--line); border-radius: 10px;\n    width: 42px; height: 42px; display: grid; place-items: center; flex: none; }\n  .card b { display: block; }\n  .card small { color: var(--muted); }\n  @keyframes reveal {\n    from { opacity: 0; transform: translateY(28px) scale(.96); }\n    to   { opacity: 1; transform: translateY(0) scale(1); }\n  }\n  \u002F* animation-range presets, toggled by JS (class on .cards) *\u002F\n  .cards.r-entry   .card { animation-range: entry 0% entry 100%; }\n  .cards.r-contain .card { animation-range: contain 0% contain 100%; }\n  .cards.r-exit    .card { animation-range: exit 0% exit 100%; }\n  .cards.r-inout   .card { animation-range: entry 0% exit 100%; }\n\n  .nosupport { display: none; border: 1px solid var(--line); border-radius: 12px;\n    padding: 18px; color: var(--muted); font-size: .9rem; line-height: 1.5; }\n  @supports not (animation-timeline: view()) {\n    .demo { display: none; }\n    .nosupport { display: block; }\n  }","\u002F\u002F The ONLY script here swaps the animation-range preset. No scroll listeners:\n  \u002F\u002F the progress bar and the reveals are driven entirely by CSS timelines.\n  const cards = document.getElementById(\"cards\");\n  const css = document.getElementById(\"css\");\n  const presets = {\n    entry:   \"entry 0% entry 100%\",\n    contain: \"contain 0% contain 100%\",\n    exit:    \"exit 0% exit 100%\",\n    inout:   \"entry 0% exit 100%\",\n  };\n  const buttons = [...document.querySelectorAll(\"[data-range]\")];\n  buttons.forEach((btn) => {\n    btn.addEventListener(\"click\", () => {\n      const key = btn.dataset.range;\n      cards.className = \"cards r-\" + key;\n      css.textContent = \"animation-range: \" + presets[key] + \";\";\n      buttons.forEach((b) => b.setAttribute(\"aria-pressed\", String(b === btn)));\n    });\n  });\n  document.getElementById(\"top\").addEventListener(\"click\", () => {\n    document.getElementById(\"scroller\").scrollTo({ top: 0 });\n  });",{"height":40,"autorun":17},520,{"slug":33,"title":42,"excerpt":43,"coverUrl":44},"Your scroll listener is doing CSS's job","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"]