[{"data":1,"prerenderedAt":1373},["ShallowReactive",2],{"content:\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fscroll-driven-animations-without-javascript":3,"surroundings:\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fscroll-driven-animations-without-javascript":1364},{"id":4,"title":5,"body":6,"description":1344,"extension":1345,"meta":1346,"navigation":1357,"path":1358,"seo":1359,"stem":1362,"__hash__":1363},"content\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fscroll-driven-animations-without-javascript\u002Findex.md","Scroll-Driven Animations Without JavaScript",{"type":7,"value":8,"toc":1326},"minimark",[9,13,33,44,157,162,195,199,205,215,221,227,231,236,428,432,585,589,685,689,692,817,821,824,828,834,961,965,984,1084,1088,1102,1106,1135,1139,1149,1162,1172,1178,1182,1194,1203,1218,1235,1244,1253,1262,1275,1284,1288,1311,1316,1319,1322],[10,11,5],"h1",{"id":12},"scroll-driven-animations-without-javascript",[14,15,16,17,22,23,27,28,32],"p",{},"This guide is part of ",[18,19,21],"a",{"href":20},"\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002F","Animation & Compositing Performance",", within ",[18,24,26],{"href":25},"\u002Frendering-css-performance\u002F","Rendering & CSS Performance",". Reading progress bars, reveal-on-scroll effects, parallax backgrounds, shrinking headers and sticky-section transitions have traditionally been built with ",[29,30,31],"code",{},"scroll"," event listeners: read the scroll position, compute a progress value, write a style. Even when carefully throttled, these handlers run on the main thread, often read layout, and lag behind the actual scroll because scrolling itself happens on the compositor. When the main thread is busy, the effect stutters or freezes while the page keeps scrolling.",[14,34,35,36,39,40,43],{},"CSS scroll-driven animations link an ordinary CSS animation to scroll progress instead of time. With ",[29,37,38],{},"animation-timeline: scroll()"," an animation tracks the scroll position of a scroller; with ",[29,41,42],{},"animation-timeline: view()"," it tracks an element's progress through the viewport. For transform and opacity animations, browsers can run these on the compositor, perfectly synchronised with scrolling and unaffected by main-thread work.",[14,45,46],{},[47,48,54,55,54,62,54,66,54,69,54,78,54,84,54,93,54,99,54,104,54,109,54,112,54,115,54,118,54,121,54,124,54,127,54,132,54,136,54,138,54,142,54,144,54,147,54,149,54,152,54,154,54],"svg",{"viewBox":49,"width":50,"role":51,"ariaLabel":52,"style":53},"0 0 760 228","100%","img","Comparison of a JavaScript scroll listener and a CSS scroll-driven animation for scroll-linked effects.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[56,57],"rect",{"className":58,"x":60,"y":60,"width":50,"height":50,"fill":61},[59],"svg-canvas","0","#ffffff",[63,64,65],"title",{},"Scroll listener vs scroll-driven animation",[67,68,52],"desc",{},[56,70],{"x":71,"y":71,"width":72,"height":73,"rx":74,"fill":75,"stroke":76,"style":77},"1","758","226","10","none","currentColor","stroke-opacity:0.18",[79,80,65],"text",{"x":81,"y":82,"fill":76,"style":83},"28.0","34.0","font-size:16px;font-weight:700",[56,85],{"x":81,"y":86,"width":87,"height":88,"rx":89,"fill":90,"stroke":91,"style":92},"56.0","340.0","150.0","6","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[79,94,98],{"x":95,"y":96,"fill":76,"style":97},"42.0","82.0","font-size:14px;font-weight:700","Scroll listener",[79,100,103],{"x":95,"y":101,"fill":76,"style":102},"108.0","font-size:12.5px;font-weight:700","•",[79,105,108],{"x":86,"y":101,"fill":76,"style":106,"textAnchor":107},"font-size:12.5px","start","Runs on the main thread per event",[79,110,103],{"x":95,"y":111,"fill":76,"style":102},"132.0",[79,113,114],{"x":86,"y":111,"fill":76,"style":106,"textAnchor":107},"Often reads layout (forced reflow risk)",[79,116,103],{"x":95,"y":117,"fill":76,"style":102},"156.0",[79,119,120],{"x":86,"y":117,"fill":76,"style":106,"textAnchor":107},"Lags or freezes when the thread is busy",[79,122,103],{"x":95,"y":123,"fill":76,"style":102},"180.0",[79,125,126],{"x":86,"y":123,"fill":76,"style":106,"textAnchor":107},"Needs throttling and cleanup",[56,128],{"x":129,"y":86,"width":87,"height":88,"rx":89,"fill":130,"stroke":130,"style":131},"392.0","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[79,133,135],{"x":134,"y":96,"fill":76,"style":97},"406.0","Scroll-driven animation",[79,137,103],{"x":134,"y":101,"fill":76,"style":102},[79,139,141],{"x":140,"y":101,"fill":76,"style":106,"textAnchor":107},"420.0","Declarative CSS, no script",[79,143,103],{"x":134,"y":111,"fill":76,"style":102},[79,145,146],{"x":140,"y":111,"fill":76,"style":106,"textAnchor":107},"Compositor-driven for transform\u002Fopacity",[79,148,103],{"x":134,"y":117,"fill":76,"style":102},[79,150,151],{"x":140,"y":117,"fill":76,"style":106,"textAnchor":107},"Stays in sync with scrolling",[79,153,103],{"x":134,"y":123,"fill":76,"style":102},[79,155,156],{"x":140,"y":123,"fill":76,"style":106,"textAnchor":107},"Progressive enhancement where unsupported",[158,159,161],"h2",{"id":160},"rapid-diagnosis","Rapid Diagnosis",[163,164,165,177,183,189],"ul",{},[166,167,168,172,173,176],"li",{},[169,170,171],"strong",{},"Search for scroll listeners"," that set styles based on ",[29,174,175],{},"scrollY"," or element positions.",[166,178,179,182],{},[169,180,181],{},"Record a trace while scrolling",": handler tasks every frame indicate main-thread scroll effects.",[166,184,185,188],{},[169,186,187],{},"Check visual lag",": effects that trail behind fast scrolls or jump when the page is busy.",[166,190,191,194],{},[169,192,193],{},"Check browser support"," for your audience; unsupported browsers need a fallback (often no animation).",[158,196,198],{"id":197},"root-cause-analysis","Root Cause Analysis",[14,200,201,204],{},[169,202,203],{},"1. Scroll is compositor-driven; listeners are not."," The page scrolls smoothly while the effect waits for the main thread.",[14,206,207,210,211,214],{},[169,208,209],{},"2. Layout reads in handlers."," Computing element progress via ",[29,212,213],{},"getBoundingClientRect()"," risks forced reflow.",[14,216,217,220],{},[169,218,219],{},"3. High event frequency."," Handlers run on every scroll frame.",[14,222,223,226],{},[169,224,225],{},"4. Main-thread contention."," Hydration, data loading and other work block the handlers.",[158,228,230],{"id":229},"step-by-step-resolution","Step-by-Step Resolution",[232,233,235],"h3",{"id":234},"_1-reading-progress-bar-with-scroll","1. Reading progress bar with scroll()",[237,238,243],"pre",{"className":239,"code":240,"language":241,"meta":242,"style":242},"language-css shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast",".progress {\n  position: fixed; inset: 0 0 auto 0; height: 4px; background: #0466c8;\n  transform-origin: left; transform: scaleX(0);\n  animation: grow linear both;\n  animation-timeline: scroll(root block);\n}\n@keyframes grow { to { transform: scaleX(1); } }\n\u002F* trade-off: browsers without support show an empty bar; wrap in @supports to\n   hide the element or keep a JS fallback only for those browsers. *\u002F\n","css","",[29,244,245,258,314,343,360,378,384,415,422],{"__ignoreMap":242},[246,247,250,254],"span",{"class":248,"line":249},"line",1,[246,251,253],{"class":252},"sPXB4",".progress",[246,255,257],{"class":256},"saISM"," {\n",[246,259,261,264,267,270,273,276,278,280,283,286,288,290,293,295,298,302,304,307,309,311],{"class":248,"line":260},2,[246,262,263],{"class":252},"  position",[246,265,266],{"class":256},": ",[246,268,269],{"class":252},"fixed",[246,271,272],{"class":256},"; ",[246,274,275],{"class":252},"inset",[246,277,266],{"class":256},[246,279,60],{"class":252},[246,281,282],{"class":252}," 0",[246,284,285],{"class":252}," auto",[246,287,282],{"class":252},[246,289,272],{"class":256},[246,291,292],{"class":252},"height",[246,294,266],{"class":256},[246,296,297],{"class":252},"4",[246,299,301],{"class":300},"sPARh","px",[246,303,272],{"class":256},[246,305,306],{"class":252},"background",[246,308,266],{"class":256},[246,310,130],{"class":252},[246,312,313],{"class":256},";\n",[246,315,317,320,322,325,327,330,332,335,338,340],{"class":248,"line":316},3,[246,318,319],{"class":252},"  transform-origin",[246,321,266],{"class":256},[246,323,324],{"class":252},"left",[246,326,272],{"class":256},[246,328,329],{"class":252},"transform",[246,331,266],{"class":256},[246,333,334],{"class":252},"scaleX",[246,336,337],{"class":256},"(",[246,339,60],{"class":252},[246,341,342],{"class":256},");\n",[246,344,346,349,352,355,358],{"class":248,"line":345},4,[246,347,348],{"class":252},"  animation",[246,350,351],{"class":256},": grow ",[246,353,354],{"class":252},"linear",[246,356,357],{"class":252}," both",[246,359,313],{"class":256},[246,361,363,366,368,370,373,376],{"class":248,"line":362},5,[246,364,365],{"class":252},"  animation-timeline",[246,367,266],{"class":256},[246,369,31],{"class":252},[246,371,372],{"class":256},"(root ",[246,374,375],{"class":252},"block",[246,377,342],{"class":256},[246,379,381],{"class":248,"line":380},6,[246,382,383],{"class":256},"}\n",[246,385,387,390,394,397,400,402,404,406,408,410,412],{"class":248,"line":386},7,[246,388,389],{"class":300},"@keyframes",[246,391,393],{"class":392},"sQw3B"," grow",[246,395,396],{"class":256}," { ",[246,398,399],{"class":252},"to",[246,401,396],{"class":256},[246,403,329],{"class":252},[246,405,266],{"class":256},[246,407,334],{"class":252},[246,409,337],{"class":256},[246,411,71],{"class":252},[246,413,414],{"class":256},"); } }\n",[246,416,418],{"class":248,"line":417},8,[246,419,421],{"class":420},"sjfSM","\u002F* trade-off: browsers without support show an empty bar; wrap in @supports to\n",[246,423,425],{"class":248,"line":424},9,[246,426,427],{"class":420},"   hide the element or keep a JS fallback only for those browsers. *\u002F\n",[232,429,431],{"id":430},"_2-reveal-on-scroll-with-view","2. Reveal-on-scroll with view()",[237,433,435],{"className":239,"code":434,"language":241,"meta":242,"style":242},"@supports (animation-timeline: view()) {\n  .reveal {\n    animation: fade-up linear both;\n    animation-timeline: view();\n    animation-range: entry 0% cover 30%;\n  }\n  @keyframes fade-up { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: none; } }\n}\n\u002F* trade-off: never apply reveal animations to above-the-fold content or the LCP\n   element; starting at opacity 0 delays when it counts as painted. *\u002F\n",[29,436,437,451,458,472,480,503,508,570,574,579],{"__ignoreMap":242},[246,438,439,442,445,448],{"class":248,"line":249},[246,440,441],{"class":300},"@supports",[246,443,444],{"class":256}," (",[246,446,447],{"class":252},"animation-timeline",[246,449,450],{"class":256},": view()) {\n",[246,452,453,456],{"class":248,"line":260},[246,454,455],{"class":252},"  .reveal",[246,457,257],{"class":256},[246,459,460,463,466,468,470],{"class":248,"line":316},[246,461,462],{"class":252},"    animation",[246,464,465],{"class":256},": fade-up ",[246,467,354],{"class":252},[246,469,357],{"class":252},[246,471,313],{"class":256},[246,473,474,477],{"class":248,"line":345},[246,475,476],{"class":252},"    animation-timeline",[246,478,479],{"class":256},": view();\n",[246,481,482,485,488,490,493,496,499,501],{"class":248,"line":362},[246,483,484],{"class":252},"    animation-range",[246,486,487],{"class":256},": entry ",[246,489,60],{"class":252},[246,491,492],{"class":300},"%",[246,494,495],{"class":252}," cover",[246,497,498],{"class":252}," 30",[246,500,492],{"class":300},[246,502,313],{"class":256},[246,504,505],{"class":248,"line":380},[246,506,507],{"class":256},"  }\n",[246,509,510,513,516,518,521,523,526,528,530,532,534,536,539,541,544,546,549,551,553,555,557,559,561,563,565,567],{"class":248,"line":386},[246,511,512],{"class":300},"  @keyframes",[246,514,515],{"class":392}," fade-up",[246,517,396],{"class":256},[246,519,520],{"class":252},"from",[246,522,396],{"class":256},[246,524,525],{"class":252},"opacity",[246,527,266],{"class":256},[246,529,60],{"class":252},[246,531,272],{"class":256},[246,533,329],{"class":252},[246,535,266],{"class":256},[246,537,538],{"class":252},"translateY",[246,540,337],{"class":256},[246,542,543],{"class":252},"24",[246,545,301],{"class":300},[246,547,548],{"class":256},"); } ",[246,550,399],{"class":252},[246,552,396],{"class":256},[246,554,525],{"class":252},[246,556,266],{"class":256},[246,558,71],{"class":252},[246,560,272],{"class":256},[246,562,329],{"class":252},[246,564,266],{"class":256},[246,566,75],{"class":252},[246,568,569],{"class":256},"; } }\n",[246,571,572],{"class":248,"line":417},[246,573,383],{"class":256},[246,575,576],{"class":248,"line":424},[246,577,578],{"class":420},"\u002F* trade-off: never apply reveal animations to above-the-fold content or the LCP\n",[246,580,582],{"class":248,"line":581},10,[246,583,584],{"class":420},"   element; starting at opacity 0 delays when it counts as painted. *\u002F\n",[232,586,588],{"id":587},"_3-parallax-with-transform-only","3. Parallax with transform only",[237,590,592],{"className":239,"code":591,"language":241,"meta":242,"style":242},"@supports (animation-timeline: scroll()) {\n  .hero-bg { animation: parallax linear both; animation-timeline: scroll(); animation-range: 0 100vh; }\n  @keyframes parallax { to { transform: translateY(30%); } }\n}\n",[29,593,594,609,653,681],{"__ignoreMap":242},[246,595,596,598,600,602,604,606],{"class":248,"line":249},[246,597,441],{"class":300},[246,599,444],{"class":256},[246,601,447],{"class":252},[246,603,266],{"class":256},[246,605,31],{"class":252},[246,607,608],{"class":256},"()) {\n",[246,610,611,614,616,619,622,624,626,628,630,632,634,637,640,642,644,647,650],{"class":248,"line":260},[246,612,613],{"class":252},"  .hero-bg",[246,615,396],{"class":256},[246,617,618],{"class":252},"animation",[246,620,621],{"class":256},": parallax ",[246,623,354],{"class":252},[246,625,357],{"class":252},[246,627,272],{"class":256},[246,629,447],{"class":252},[246,631,266],{"class":256},[246,633,31],{"class":252},[246,635,636],{"class":256},"(); ",[246,638,639],{"class":252},"animation-range",[246,641,266],{"class":256},[246,643,60],{"class":252},[246,645,646],{"class":252}," 100",[246,648,649],{"class":300},"vh",[246,651,652],{"class":256},"; }\n",[246,654,655,657,660,662,664,666,668,670,672,674,677,679],{"class":248,"line":316},[246,656,512],{"class":300},[246,658,659],{"class":392}," parallax",[246,661,396],{"class":256},[246,663,399],{"class":252},[246,665,396],{"class":256},[246,667,329],{"class":252},[246,669,266],{"class":256},[246,671,538],{"class":252},[246,673,337],{"class":256},[246,675,676],{"class":252},"30",[246,678,492],{"class":300},[246,680,414],{"class":256},[246,682,683],{"class":248,"line":345},[246,684,383],{"class":256},[232,686,688],{"id":687},"_4-remove-the-old-listeners","4. Remove the old listeners",[14,690,691],{},"Delete the scroll handlers (and their throttle utilities) once the CSS versions are in place, keeping a JavaScript fallback only if the effect is essential in unsupported browsers.",[14,693,694],{},[47,695,54,698,54,701,54,704,54,706,54,709,54,711,54,715,54,722,54,729,54,733,54,738,54,743,54,745,54,748,54,751,54,755,54,758,54,761,54,766,54,772,54,778,54,782,54,786,54,790,54,794,54,797,54,801,54,805,54,809,54,813,54],{"viewBox":696,"width":50,"role":51,"ariaLabel":697,"style":53},"0 0 760 216","Timeline showing a scroll listener effect freezing during a long task while a scroll-driven animation keeps updating.",[56,699],{"className":700,"x":60,"y":60,"width":50,"height":50,"fill":61},[59],[63,702,703],{},"Scroll effect under main-thread load",[67,705,697],{},[56,707],{"x":71,"y":71,"width":72,"height":708,"rx":74,"fill":75,"stroke":76,"style":77},"214",[79,710,703],{"x":81,"y":82,"fill":76,"style":83},[79,712,714],{"x":81,"y":713,"fill":76,"style":102},"74.0","Main thread",[56,716],{"x":717,"y":86,"width":718,"height":719,"rx":720,"fill":75,"stroke":76,"style":721},"325.7","199.0","26.0","3","stroke-opacity:0.35",[79,723,728],{"x":724,"y":725,"fill":76,"style":726,"textAnchor":727},"425.2","73.5","font-size:11.5px","middle","long task (hydration)",[79,730,732],{"x":81,"y":731,"fill":76,"style":102},"112.0","Listener-driven bar",[56,734],{"x":735,"y":736,"width":737,"height":719,"rx":720,"fill":130,"stroke":130,"style":131},"153.8","94.0","170.4",[79,739,742],{"x":740,"y":741,"fill":76,"style":726,"textAnchor":727},"239.0","111.5","updates",[56,744],{"x":717,"y":736,"width":718,"height":719,"rx":720,"fill":75,"stroke":76,"style":721},[79,746,747],{"x":724,"y":741,"fill":76,"style":726,"textAnchor":727},"frozen",[56,749],{"x":750,"y":736,"width":718,"height":719,"rx":720,"fill":130,"stroke":130,"style":131},"526.2",[79,752,754],{"x":753,"y":741,"fill":76,"style":726,"textAnchor":727},"625.7","catches up",[79,756,757],{"x":81,"y":88,"fill":76,"style":102},"Scroll-driven bar",[56,759],{"x":735,"y":111,"width":760,"height":719,"rx":720,"fill":90,"stroke":91,"style":92},"571.4",[79,762,765],{"x":763,"y":764,"fill":76,"style":726,"textAnchor":727},"439.5","149.5","updates every frame on compositor",[248,767],{"x1":768,"y1":769,"x2":770,"y2":769,"stroke":76,"style":771},"153.1","172.0","726.0","stroke-opacity:0.4",[79,773,777],{"x":774,"y":775,"fill":776,"style":726,"textAnchor":727},"163.8","189.0","#51617a","0ms",[79,779,781],{"x":780,"y":775,"fill":776,"style":726,"textAnchor":727},"210.4","100ms",[79,783,785],{"x":784,"y":775,"fill":776,"style":726,"textAnchor":727},"267.7","200ms",[79,787,789],{"x":788,"y":775,"fill":776,"style":726,"textAnchor":727},"324.9","300ms",[79,791,793],{"x":792,"y":775,"fill":776,"style":726,"textAnchor":727},"382.2","400ms",[79,795,796],{"x":763,"y":775,"fill":776,"style":726,"textAnchor":727},"500ms",[79,798,800],{"x":799,"y":775,"fill":776,"style":726,"textAnchor":727},"496.8","600ms",[79,802,804],{"x":803,"y":775,"fill":776,"style":726,"textAnchor":727},"554.1","700ms",[79,806,808],{"x":807,"y":775,"fill":776,"style":726,"textAnchor":727},"611.4","800ms",[79,810,812],{"x":811,"y":775,"fill":776,"style":726,"textAnchor":727},"668.7","900ms",[79,814,816],{"x":815,"y":775,"fill":776,"style":726,"textAnchor":727},"712.3","1000ms",[158,818,820],{"id":819},"verification","Verification",[14,822,823],{},"Record a trace while scrolling: no scroll handler tasks should appear, and the effect should remain in sync during a simulated long task (run a busy loop in the console while scrolling). Check that above-the-fold content is visible immediately on load. Test in a browser without support to confirm the fallback is acceptable.",[158,825,827],{"id":826},"worked-example-a-long-read-feature","Worked Example: A Long-Read Feature",[14,829,830,831,833],{},"A publisher's long-read template had a reading progress bar, section reveal animations and a parallax hero, all driven by one scroll handler that read the positions of 30 elements on each frame. On mid-range phones, scrolling showed 20–30ms handler tasks and visible stutter; INP suffered when taps landed during scroll handling. Rewriting all three effects as scroll-driven animations (with ",[29,832,441],{}," guards and no fallback for reveals, since content was simply visible without them) removed the handler. Scroll frame drops disappeared in supporting browsers, and INP p75 on the template improved by 60ms.",[14,835,836],{},[47,837,54,840,54,843,54,846,54,848,54,851,54,853,54,858,54,863,54,867,54,871,54,874,54,878,54,881,54,885,54,887,54,891,54,893,54,896,54,899,54,903,54,905,54,908,54,910,54,913,54,916,54,920,54,922,54,925,54,927,54,929,54,932,54,936,54,938,54,940,54,942,54,945,54,948,54,952,54,954,54,957,54,959,54],{"viewBox":838,"width":50,"role":51,"ariaLabel":839,"style":53},"0 0 760 258","Common scroll-linked effects and the scroll-driven animation features that replace JavaScript implementations.",[56,841],{"className":842,"x":60,"y":60,"width":50,"height":50,"fill":61},[59],[63,844,845],{},"Scroll effects and their CSS replacements",[67,847,839],{},[56,849],{"x":71,"y":71,"width":72,"height":850,"rx":74,"fill":75,"stroke":76,"style":77},"256",[79,852,845],{"x":81,"y":82,"fill":76,"style":83},[56,854],{"x":81,"y":86,"width":855,"height":856,"rx":60,"fill":76,"stroke":76,"style":857},"163.0","30.0","fill-opacity:0.06;stroke-opacity:0.4",[79,859,862],{"x":860,"y":861,"fill":76,"style":102,"textAnchor":107},"38.0","75.5","Effect",[56,864],{"x":865,"y":86,"width":866,"height":856,"rx":60,"fill":76,"stroke":76,"style":857},"191.0","270.5",[79,868,870],{"x":869,"y":861,"fill":76,"style":102,"textAnchor":727},"326.2","CSS feature",[56,872],{"x":873,"y":86,"width":866,"height":856,"rx":60,"fill":76,"stroke":76,"style":857},"461.5",[79,875,877],{"x":876,"y":861,"fill":76,"style":102,"textAnchor":727},"596.7","Properties",[56,879],{"x":81,"y":880,"width":855,"height":856,"rx":60,"fill":75,"stroke":76,"style":721},"86.0",[79,882,884],{"x":860,"y":883,"fill":76,"style":102,"textAnchor":107},"105.5","Reading progress bar",[56,886],{"x":865,"y":880,"width":866,"height":856,"rx":60,"fill":75,"stroke":76,"style":721},[79,888,890],{"x":869,"y":883,"fill":76,"style":889,"textAnchor":727},"font-size:12px","scroll(root)",[56,892],{"x":873,"y":880,"width":866,"height":856,"rx":60,"fill":130,"stroke":130,"style":131},[79,894,895],{"x":876,"y":883,"fill":76,"style":889,"textAnchor":727},"transform scaleX",[56,897],{"x":81,"y":898,"width":855,"height":856,"rx":60,"fill":75,"stroke":76,"style":721},"116.0",[79,900,902],{"x":860,"y":901,"fill":76,"style":102,"textAnchor":107},"135.5","Reveal on scroll",[56,904],{"x":865,"y":898,"width":866,"height":856,"rx":60,"fill":75,"stroke":76,"style":721},[79,906,907],{"x":869,"y":901,"fill":76,"style":889,"textAnchor":727},"view() + animation-range",[56,909],{"x":873,"y":898,"width":866,"height":856,"rx":60,"fill":130,"stroke":130,"style":131},[79,911,912],{"x":876,"y":901,"fill":76,"style":889,"textAnchor":727},"opacity, translate",[56,914],{"x":81,"y":915,"width":855,"height":856,"rx":60,"fill":75,"stroke":76,"style":721},"146.0",[79,917,919],{"x":860,"y":918,"fill":76,"style":102,"textAnchor":107},"165.5","Parallax background",[56,921],{"x":865,"y":915,"width":866,"height":856,"rx":60,"fill":75,"stroke":76,"style":721},[79,923,924],{"x":869,"y":918,"fill":76,"style":889,"textAnchor":727},"scroll() + range",[56,926],{"x":873,"y":915,"width":866,"height":856,"rx":60,"fill":130,"stroke":130,"style":131},[79,928,538],{"x":876,"y":918,"fill":76,"style":889,"textAnchor":727},[56,930],{"x":81,"y":931,"width":855,"height":856,"rx":60,"fill":75,"stroke":76,"style":721},"176.0",[79,933,935],{"x":860,"y":934,"fill":76,"style":102,"textAnchor":107},"195.5","Shrinking sticky header",[56,937],{"x":865,"y":931,"width":866,"height":856,"rx":60,"fill":75,"stroke":76,"style":721},[79,939,924],{"x":869,"y":934,"fill":76,"style":889,"textAnchor":727},[56,941],{"x":873,"y":931,"width":866,"height":856,"rx":60,"fill":76,"stroke":76,"style":857},[79,943,944],{"x":876,"y":934,"fill":76,"style":889,"textAnchor":727},"scale; avoid height",[56,946],{"x":81,"y":947,"width":855,"height":856,"rx":60,"fill":75,"stroke":76,"style":721},"206.0",[79,949,951],{"x":860,"y":950,"fill":76,"style":102,"textAnchor":107},"225.5","Gallery position indicator",[56,953],{"x":865,"y":947,"width":866,"height":856,"rx":60,"fill":75,"stroke":76,"style":721},[79,955,956],{"x":869,"y":950,"fill":76,"style":889,"textAnchor":727},"named scroll-timeline",[56,958],{"x":873,"y":947,"width":866,"height":856,"rx":60,"fill":130,"stroke":130,"style":131},[79,960,329],{"x":876,"y":950,"fill":76,"style":889,"textAnchor":727},[158,962,964],{"id":963},"named-timelines-and-sticky-sections","Named Timelines and Sticky Sections",[14,966,967,968,971,972,975,976,979,980,983],{},"For effects that depend on a specific scroller or element other than the animated one, named timelines connect them. ",[29,969,970],{},"scroll-timeline-name"," on a scroller or ",[29,973,974],{},"view-timeline-name"," on a tracked element creates a timeline that other elements can reference with ",[29,977,978],{},"animation-timeline: --name",". Use ",[29,981,982],{},"timeline-scope"," on a common ancestor to make a named timeline visible to elements outside the subtree. This enables effects like a sticky header that changes as a particular section passes, or a horizontal gallery whose indicator tracks its own scroll, all without scripting.",[237,985,987],{"className":239,"code":986,"language":241,"meta":242,"style":242},".gallery { overflow-x: auto; scroll-timeline: --gallery inline; }\n.gallery-indicator { animation: fill linear both; animation-timeline: --gallery; }\n@keyframes fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }\n",[29,988,989,1017,1043],{"__ignoreMap":242},[246,990,991,994,996,999,1001,1004,1006,1009,1012,1015],{"class":248,"line":249},[246,992,993],{"class":252},".gallery",[246,995,396],{"class":256},[246,997,998],{"class":252},"overflow-x",[246,1000,266],{"class":256},[246,1002,1003],{"class":252},"auto",[246,1005,272],{"class":256},[246,1007,1008],{"class":252},"scroll-timeline",[246,1010,1011],{"class":256},": --gallery ",[246,1013,1014],{"class":252},"inline",[246,1016,652],{"class":256},[246,1018,1019,1022,1024,1026,1028,1031,1034,1036,1038,1040],{"class":248,"line":260},[246,1020,1021],{"class":252},".gallery-indicator",[246,1023,396],{"class":256},[246,1025,618],{"class":252},[246,1027,266],{"class":256},[246,1029,1030],{"class":252},"fill",[246,1032,1033],{"class":252}," linear",[246,1035,357],{"class":252},[246,1037,272],{"class":256},[246,1039,447],{"class":252},[246,1041,1042],{"class":256},": --gallery; }\n",[246,1044,1045,1047,1050,1052,1054,1056,1058,1060,1062,1064,1066,1068,1070,1072,1074,1076,1078,1080,1082],{"class":248,"line":316},[246,1046,389],{"class":300},[246,1048,1049],{"class":392}," fill",[246,1051,396],{"class":256},[246,1053,520],{"class":252},[246,1055,396],{"class":256},[246,1057,329],{"class":252},[246,1059,266],{"class":256},[246,1061,334],{"class":252},[246,1063,337],{"class":256},[246,1065,60],{"class":252},[246,1067,548],{"class":256},[246,1069,399],{"class":252},[246,1071,396],{"class":256},[246,1073,329],{"class":252},[246,1075,266],{"class":256},[246,1077,334],{"class":252},[246,1079,337],{"class":256},[246,1081,71],{"class":252},[246,1083,414],{"class":256},[158,1085,1087],{"id":1086},"rolling-out-as-a-progressive-enhancement","Rolling Out as a Progressive Enhancement",[14,1089,1090,1091,1094,1095,1097,1098,1101],{},"Scroll-driven animations are ideal progressive enhancements: when unsupported, the content should simply appear in its final state. Write the base styles for the final state, then add the animation inside ",[29,1092,1093],{},"@supports (animation-timeline: scroll())",". Never set an element's initial state to invisible outside the ",[29,1096,441],{}," block, or unsupported browsers will hide it forever. For essential effects, load a JavaScript fallback only when ",[29,1099,1100],{},"CSS.supports('animation-timeline: scroll()')"," is false, so supporting browsers never download or run the old handler.",[158,1103,1105],{"id":1104},"common-mistakes","Common Mistakes",[163,1107,1108,1114,1120,1129],{},[166,1109,1110,1113],{},[169,1111,1112],{},"Animating layout properties on a scroll timeline."," They still run on the main thread; use transform and opacity.",[166,1115,1116,1119],{},[169,1117,1118],{},"Reveal animations on above-the-fold content."," Delays LCP and makes the first view look empty.",[166,1121,1122,1128],{},[169,1123,1124,1125,1127],{},"No ",[29,1126,441],{}," guard."," Unsupported browsers may leave elements in their initial keyframe state.",[166,1130,1131,1134],{},[169,1132,1133],{},"Keeping old listeners running."," Double work and conflicting styles.",[158,1136,1138],{"id":1137},"edge-cases","Edge Cases",[14,1140,1141,1144,1145,1148],{},[169,1142,1143],{},"Reduced motion."," Respect ",[29,1146,1147],{},"prefers-reduced-motion"," by disabling scroll-driven animations or replacing movement with simple fades.",[14,1150,1151,54,1154,1157,1158,1161],{},[169,1152,1153],{},"Nested scrollers.",[29,1155,1156],{},"scroll()"," defaults to the nearest scroll ancestor; specify ",[29,1159,1160],{},"root"," or use named timelines for the intended scroller.",[14,1163,1164,1167,1168,1171],{},[169,1165,1166],{},"Animation fill."," Use ",[29,1169,1170],{},"both"," fill mode so elements hold their start state before the range and their end state after it.",[14,1173,1174,1177],{},[169,1175,1176],{},"Accessibility of hidden content."," Content that starts at opacity 0 is still in the accessibility tree; ensure it becomes visible when focused.",[158,1179,1181],{"id":1180},"faq","FAQ",[1183,1184,1187,1191],"details",{"className":1185},[1186],"faq-item",[1188,1189,1190],"summary",{},"Which browsers support scroll-driven animations?",[14,1192,1193],{},"Chromium-based browsers support them, and support has been arriving in other engines. Treat them as a progressive enhancement and check current support for your audience.",[1183,1195,1197,1200],{"className":1196},[1186],[1188,1198,1199],{},"Do scroll-driven animations run on the compositor?",[14,1201,1202],{},"For compositor-friendly properties like transform and opacity, browsers can run them off the main thread, keeping them in sync with scrolling.",[1183,1204,1206,1209],{"className":1205},[1186],[1188,1207,1208],{},"What is the difference between scroll() and view()?",[14,1210,1211,1213,1214,1217],{},[29,1212,1156],{}," tracks the scroll position of a scroll container from start to end. ",[29,1215,1216],{},"view()"," tracks an element's visibility as it passes through a scroller's viewport.",[1183,1219,1221,1224],{"className":1220},[1186],[1188,1222,1223],{},"Can I use scroll-driven animations with JavaScript?",[14,1225,1226,1227,1230,1231,1234],{},"Yes. The Web Animations API accepts ",[29,1228,1229],{},"ScrollTimeline"," and ",[29,1232,1233],{},"ViewTimeline"," objects, so you can create them in script while keeping the compositor benefits.",[1183,1236,1238,1241],{"className":1237},[1186],[1188,1239,1240],{},"Do they affect CLS?",[14,1242,1243],{},"Transform and opacity animations do not cause layout shifts. Animating layout properties on a scroll timeline can.",[1183,1245,1247,1250],{"className":1246},[1186],[1188,1248,1249],{},"What fallback should I use?",[14,1251,1252],{},"For decorative effects, no animation. For essential indicators like reading progress, a lightweight JavaScript fallback limited to unsupported browsers.",[1183,1254,1256,1259],{"className":1255},[1186],[1188,1257,1258],{},"Can scroll-driven animations replace IntersectionObserver?",[14,1260,1261],{},"For visual effects, often. For logic such as loading content or analytics when elements become visible, IntersectionObserver is still the right tool.",[1183,1263,1265,1268],{"className":1264},[1186],[1188,1266,1267],{},"Do scroll-driven animations work with smooth scrolling?",[14,1269,1270,1271,1274],{},"Yes. They follow the actual scroll position, whether the user scrolls with a wheel, touch, keyboard or ",[29,1272,1273],{},"scroll-behavior: smooth",", so they stay synchronised in every case.",[1183,1276,1278,1281],{"className":1277},[1186],[1188,1279,1280],{},"Are scroll-driven animations accessible?",[14,1282,1283],{},"They can be, if they respect reduced motion and never hide content that users need. Content should be readable even when the animation is disabled or unsupported.",[158,1285,1287],{"id":1286},"related","Related",[163,1289,1290,1297,1304],{},[166,1291,1292,1296],{},[18,1293,1295],{"href":1294},"\u002Frendering-css-performance\u002Flayout-thrashing-and-forced-reflow\u002Fresizeobserver-vs-resize-and-scroll-handlers\u002F","ResizeObserver vs resize and scroll handlers"," — replacing other scroll handlers.",[166,1298,1299,1303],{},[18,1300,1302],{"href":1301},"\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fanimating-only-transform-and-opacity\u002F","Animating only transform and opacity"," — properties to animate on timelines.",[166,1305,1306,1310],{},[18,1307,1309],{"href":1308},"\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fview-transitions-api-performance\u002F","View Transitions API performance"," — the other modern animation API.",[1312,1313,1315],"script",{"type":1314},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Scroll-Driven Animations Without JavaScript\",\n  \"description\": \"How CSS scroll-driven animations work, how they replace scroll listeners for common effects, and how to use them as a progressive enhancement.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Reading progress bar with scroll()\",\n      \"text\": \"Reading progress bar with scroll()\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Reveal-on-scroll with view()\",\n      \"text\": \"Reveal-on-scroll with view()\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Parallax with transform only\",\n      \"text\": \"Parallax with transform only\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Remove the old listeners\",\n      \"text\": \"Delete the scroll handlers (and their throttle utilities) once the CSS versions are in place, keeping a JavaScript fallback only if the effect is essential in unsupported browsers.\"\n    }\n  ]\n}\n",[1312,1317,1318],{"type":1314},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"Scroll-Driven Animations Without JavaScript\",\n  \"description\": \"How CSS scroll-driven animations work, how they replace scroll listeners for common effects, and how to use them as a progressive enhancement.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fscroll-driven-animations-without-javascript\u002F\"\n  }\n}\n",[1312,1320,1321],{"type":1314},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"Rendering & CSS Performance\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Frendering-css-performance\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Animation & Compositing Performance\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Scroll-Driven Animations Without JavaScript\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fscroll-driven-animations-without-javascript\u002F\"\n    }\n  ]\n}\n",[1323,1324,1325],"style",{},"html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}",{"title":242,"searchDepth":260,"depth":260,"links":1327},[1328,1329,1330,1336,1337,1338,1339,1340,1341,1342,1343],{"id":160,"depth":260,"text":161},{"id":197,"depth":260,"text":198},{"id":229,"depth":260,"text":230,"children":1331},[1332,1333,1334,1335],{"id":234,"depth":316,"text":235},{"id":430,"depth":316,"text":431},{"id":587,"depth":316,"text":588},{"id":687,"depth":316,"text":688},{"id":819,"depth":260,"text":820},{"id":826,"depth":260,"text":827},{"id":963,"depth":260,"text":964},{"id":1086,"depth":260,"text":1087},{"id":1104,"depth":260,"text":1105},{"id":1137,"depth":260,"text":1138},{"id":1180,"depth":260,"text":1181},{"id":1286,"depth":260,"text":1287},"How CSS scroll-driven animations work, how they replace scroll listeners for common effects, and how to use them as a progressive enhancement.","md",{"slug":12,"type":1347,"breadcrumb":1348,"datePublished":1356,"dateModified":1356},"article",[1349,1352,1353,1354],{"name":1350,"url":1351},"Home","\u002F",{"name":26,"url":25},{"name":21,"url":20},{"name":5,"url":1355},"\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fscroll-driven-animations-without-javascript\u002F","2026-10-06",true,"\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fscroll-driven-animations-without-javascript",{"title":1360,"description":1361},"Scroll-Driven Animations in CSS Without JavaScript","Replace scroll listeners with CSS animation-timeline: scroll() and view() for progress bars, reveals and parallax that run off the main thread.","rendering-css-performance\u002Fanimation-and-compositing-performance\u002Fscroll-driven-animations-without-javascript\u002Findex","JlpQ2fl2395x81r2XO2cNde9uxUtb7oKttSTY6o6wQo",[1365,1369],{"title":1366,"path":1367,"stem":1368},"Animating Only transform and opacity","\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fanimating-only-transform-and-opacity","rendering-css-performance\u002Fanimation-and-compositing-performance\u002Fanimating-only-transform-and-opacity\u002Findex",{"title":1370,"path":1371,"stem":1372},"View Transitions API Performance","\u002Frendering-css-performance\u002Fanimation-and-compositing-performance\u002Fview-transitions-api-performance","rendering-css-performance\u002Fanimation-and-compositing-performance\u002Fview-transitions-api-performance\u002Findex",1791308078008]