[{"data":1,"prerenderedAt":1962},["ShallowReactive",2],{"page-\u002Fcss-only-micro-interactions-animations\u002Fscroll-driven-animations\u002Fscroll-progress-bar-without-javascript\u002F":3,"content-all-pages":1700},{"id":4,"title":5,"body":6,"description":1682,"extension":1683,"meta":1684,"navigation":98,"path":1696,"seo":1697,"stem":1698,"__hash__":1699},"content\u002Fcss-only-micro-interactions-animations\u002Fscroll-driven-animations\u002Fscroll-progress-bar-without-javascript\u002Findex.md","A Reading Progress Bar Driven by scroll(root)",{"type":7,"value":8,"toc":1673},"minimark",[9,13,32,35,40,43,189,192,204,215,218,221,224,334,336,340,343,347,1092,1106,1108,1112,1139,1142,1156,1237,1239,1243,1252,1436,1452,1454,1458,1475,1565,1573,1575,1579,1589,1602,1608,1614,1616,1620,1670],[10,11,5],"h1",{"id":12},"a-reading-progress-bar-driven-by-scrollroot",[14,15,16,17,21,22,25,26,31],"p",{},"A reading-progress bar is the smallest useful scroll-linked effect: a strip across the top of the page whose fill maps directly onto how far down the document the reader has travelled. The narrow problem this page solves is producing that bar with no script at all — one element, one ",[18,19,20],"code",{},"@keyframes"," rule, and ",[18,23,24],{},"animation-timeline: scroll(root)"," — and doing it in a way that stays glued to the scrollbar during a fast flick, which is precisely where the traditional listener implementation falls apart. This is the simplest entry point into ",[27,28,30],"a",{"href":29},"\u002Fcss-only-micro-interactions-animations\u002Fscroll-driven-animations\u002F","scroll-driven animations",", because the timeline you need is the one the browser already maintains for the document.",[33,34],"hr",{},[36,37,39],"h2",{"id":38},"why-not-the-listener-version","Why Not the Listener Version",[14,41,42],{},"Here is the implementation this page replaces, in its most common form:",[44,45,50],"pre",{"className":46,"code":47,"language":48,"meta":49,"style":49},"language-js shiki shiki-themes github-light-high-contrast github-dark-high-contrast","\u002F\u002F The approach being replaced.\nconst bar = document.querySelector('.progress');\n\nwindow.addEventListener('scroll', () => {\n  const max = document.documentElement.scrollHeight - window.innerHeight;\n  const pct = window.scrollY \u002F max;\n  bar.style.transform = `scaleX(${pct})`;\n});\n","js","",[18,51,52,61,93,100,123,143,162,183],{"__ignoreMap":49},[53,54,57],"span",{"class":55,"line":56},"line",1,[53,58,60],{"class":59},"sLBg1","\u002F\u002F The approach being replaced.\n",[53,62,64,68,72,75,79,83,86,90],{"class":55,"line":63},2,[53,65,67],{"class":66},"sHUrx","const",[53,69,71],{"class":70},"s-5SL"," bar",[53,73,74],{"class":66}," =",[53,76,78],{"class":77},"suds8"," document.",[53,80,82],{"class":81},"sKwhi","querySelector",[53,84,85],{"class":77},"(",[53,87,89],{"class":88},"sT6z2","'.progress'",[53,91,92],{"class":77},");\n",[53,94,96],{"class":55,"line":95},3,[53,97,99],{"emptyLinePlaceholder":98},true,"\n",[53,101,103,106,109,111,114,117,120],{"class":55,"line":102},4,[53,104,105],{"class":77},"window.",[53,107,108],{"class":81},"addEventListener",[53,110,85],{"class":77},[53,112,113],{"class":88},"'scroll'",[53,115,116],{"class":77},", () ",[53,118,119],{"class":66},"=>",[53,121,122],{"class":77}," {\n",[53,124,126,129,132,134,137,140],{"class":55,"line":125},5,[53,127,128],{"class":66},"  const",[53,130,131],{"class":70}," max",[53,133,74],{"class":66},[53,135,136],{"class":77}," document.documentElement.scrollHeight ",[53,138,139],{"class":66},"-",[53,141,142],{"class":77}," window.innerHeight;\n",[53,144,146,148,151,153,156,159],{"class":55,"line":145},6,[53,147,128],{"class":66},[53,149,150],{"class":70}," pct",[53,152,74],{"class":66},[53,154,155],{"class":77}," window.scrollY ",[53,157,158],{"class":66},"\u002F",[53,160,161],{"class":77}," max;\n",[53,163,165,168,171,174,177,180],{"class":55,"line":164},7,[53,166,167],{"class":77},"  bar.style.transform ",[53,169,170],{"class":66},"=",[53,172,173],{"class":88}," `scaleX(${",[53,175,176],{"class":77},"pct",[53,178,179],{"class":88},"})`",[53,181,182],{"class":77},";\n",[53,184,186],{"class":55,"line":185},8,[53,187,188],{"class":77},"});\n",[14,190,191],{},"It is eight lines and it works. It also has four structural problems that no amount of tuning removes.",[14,193,194,195,198,199,203],{},"It runs on the main thread. Scroll input is processed by the compositor, but a ",[18,196,197],{},"scroll"," event is dispatched to the main thread, so the bar's position is computed ",[200,201,202],"em",{},"after"," the scroll has already been painted. The bar is permanently at least one frame behind the content, and during any long task — hydration, a big JSON parse, an ad script — it stops updating entirely while the page keeps scrolling. On a fast flick this reads as the bar visibly chasing the page.",[14,205,206,207,210,211,214],{},"It reads layout every frame. ",[18,208,209],{},"scrollHeight"," and ",[18,212,213],{},"innerHeight"," are layout-dependent, so unless you cache them you have written a forced synchronous layout into the hottest callback on the page. Caching them, in turn, means you now have to invalidate the cache on resize, on font load, and on any DOM mutation that changes document height.",[14,216,217],{},"It is bytes and a lifecycle. The listener must be registered, and in a component framework it must also be removed, and if the page is server-rendered the bar is wrong until hydration finishes.",[14,219,220],{},"And it does nothing useful when JavaScript fails. The bar sits at zero, which is worse than no bar at all, because it actively communicates false information.",[14,222,223],{},"The CSS version has none of these properties. The timeline is evaluated where the scroll offset already lives, so the bar cannot lag; there is no layout read, because the browser is not measuring anything it did not already know; there is no lifecycle; and when the feature is unsupported the declaration is simply not applied, which is a state you get to design.",[225,226,232,233,232,237,232,241,232,248,232,254,232,259,232,271,232,274,232,282,232,287,232,290,232,294,232,297,232,301,232,304,232,307,232,312,232,315,232,317,232,320,232,322,232,325,232,327,232,330],"svg",{"viewBox":227,"role":228,"ariaLabel":229,"xmlns":230,"style":231},"0 0 720 340","img","Two columns comparing the work a scroll listener does on the main thread against the work a scroll timeline does on the compositor","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;max-width:720px;margin:2rem 0","\n  ",[234,235,236],"title",{},"Listener path versus scroll timeline path",[238,239,240],"desc",{},"Left column lists four main-thread steps for a scroll event listener; right column lists the equivalent compositor steps for an animation bound to a scroll progress timeline.",[242,243,247],"text",{"x":244,"y":245,"style":246},"360","26","text-anchor:middle;fill:currentColor;font:700 17px sans-serif","Two ways to draw a progress bar",[242,249,253],{"x":250,"y":251,"style":252},"184","52","text-anchor:middle;fill:currentColor;font:600 13px sans-serif;opacity:0.85","scroll event listener",[242,255,258],{"x":256,"y":251,"style":257},"536","text-anchor:middle;fill:currentColor;font:12px ui-monospace,monospace","animation-timeline: scroll()",[260,261],"rect",{"x":262,"y":263,"width":264,"height":265,"rx":266,"fill":267,"stroke":268,"strokeWidth":269,"opacity":270},"24","64","320","230","12","none","currentColor","1.5","0.45",[260,272],{"x":273,"y":263,"width":264,"height":265,"rx":266,"fill":267,"stroke":268,"strokeWidth":269,"opacity":270},"376",[260,275],{"x":276,"y":277,"width":278,"height":279,"rx":280,"fill":268,"opacity":281},"54","100","260","34","6","0.1",[242,283,286],{"x":250,"y":284,"style":285},"122","text-anchor:middle;fill:currentColor;font:12px sans-serif","scroll event dispatched",[260,288],{"x":276,"y":289,"width":278,"height":279,"rx":280,"fill":268,"opacity":281},"146",[242,291,293],{"x":250,"y":292,"style":285},"168","read scrollY and height",[260,295],{"x":276,"y":296,"width":278,"height":279,"rx":280,"fill":268,"opacity":281},"192",[242,298,300],{"x":250,"y":299,"style":285},"214","write inline transform",[260,302],{"x":276,"y":303,"width":278,"height":279,"rx":280,"fill":268,"opacity":281},"238",[242,305,306],{"x":250,"y":278,"style":285},"one frame behind",[260,308],{"x":309,"y":277,"width":278,"height":279,"rx":280,"fill":310,"opacity":311},"406","#7aa2ff","0.2",[242,313,314],{"x":256,"y":284,"style":285},"compositor scrolls",[260,316],{"x":309,"y":289,"width":278,"height":279,"rx":280,"fill":310,"opacity":311},[242,318,319],{"x":256,"y":292,"style":285},"timeline sampled there",[260,321],{"x":309,"y":296,"width":278,"height":279,"rx":280,"fill":310,"opacity":311},[242,323,324],{"x":256,"y":299,"style":285},"scaleX applied",[260,326],{"x":309,"y":303,"width":278,"height":279,"rx":280,"fill":310,"opacity":311},[242,328,329],{"x":256,"y":278,"style":285},"same frame as the scroll",[242,331,333],{"x":244,"y":264,"style":332},"text-anchor:middle;fill:currentColor;font:12px sans-serif;opacity:0.7","The right-hand path never touches the main thread.",[33,335],{},[36,337,339],{"id":338},"complete-working-implementation","Complete Working Implementation",[14,341,342],{},"Scroll the frame below. The bar is bound to the frame document's own scroller, so it fills as you go and empties as you come back — no listener involved.",[344,345],"live-demo",{"name":346},"scroll-progress-bar",[44,348,352],{"className":349,"code":350,"language":351,"meta":49,"style":49},"language-html shiki shiki-themes github-light-high-contrast github-dark-high-contrast","\u003C!doctype html>\n\u003Chtml lang=\"en\">\n\u003Chead>\n\u003Cmeta charset=\"utf-8\">\n\u003Cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n\u003Ctitle>Reading progress\u003C\u002Ftitle>\n\u003Cstyle>\n  body { margin: 0; font: 16px\u002F1.6 system-ui, sans-serif; }\n\n  .progress {\n    position: fixed;\n    inset-block-start: 0;\n    inset-inline: 0;\n    z-index: 3;\n    height: 5px;\n    background: #4361c8;\n    \u002F* Scale from the left edge, so scaleX(0.4) fills the first 40%. *\u002F\n    transform-origin: 0 50%;\n    \u002F* `linear` is mandatory: any easing curve would distort the mapping\n       from scroll position onto animation progress. *\u002F\n    animation: grow-progress linear;\n    \u002F* `root` = the document's own scroller (not the nearest ancestor).\n       `block` = the document's block axis, vertical in this writing mode. *\u002F\n    animation-timeline: scroll(root block);\n  }\n\n  @keyframes grow-progress {\n    from { transform: scaleX(0); }\n    to   { transform: scaleX(1); }\n  }\n\n  \u002F* The bar carries position information, so it must never disappear.\n     Under reduced motion we keep it and drop only the decorative colour\n     travel; the fill itself is not a vestibular risk at 5px tall. *\u002F\n  @media (prefers-reduced-motion: reduce) {\n    .progress { background: #4361c8; }\n  }\n\n  .article { max-width: 40rem; margin: 0 auto; padding: 2rem 1.25rem 60vh; }\n\u003C\u002Fstyle>\n\u003C\u002Fhead>\n\u003Cbody>\n  \u003C!-- role=\"presentation\": the bar duplicates information the scrollbar\n       already exposes to assistive technology. Do not announce it twice. -->\n  \u003Cdiv class=\"progress\" role=\"presentation\">\u003C\u002Fdiv>\n\n  \u003Carticle class=\"article\">\n    \u003Ch1>The Compositor Never Blinks\u003C\u002Fh1>\n    \u003Cp>Scroll this document and watch the bar at the top.\u003C\u002Fp>\n    \u003Cp>Its progress is not being computed by any script.\u003C\u002Fp>\n    \u003Cp>It is a keyframe animation whose timeline is the scroller itself.\u003C\u002Fp>\n    \u003Cp>Because the compositor already owns the scroll offset, sampling that\n       timeline costs nothing on the main thread.\u003C\u002Fp>\n    \u003Cp>Block the main thread with a long task and the bar keeps pace.\u003C\u002Fp>\n    \u003Cp>A scroll listener would freeze.\u003C\u002Fp>\n  \u003C\u002Farticle>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n","html",[18,353,354,369,386,395,412,436,449,458,506,511,519,532,544,556,569,584,597,603,621,627,633,647,653,659,677,683,688,700,723,745,750,755,761,767,773,782,799,804,809,864,874,883,893,899,905,937,942,959,974,988,1002,1016,1026,1036,1050,1064,1074,1083],{"__ignoreMap":49},[53,355,356,359,363,366],{"class":55,"line":56},[53,357,358],{"class":77},"\u003C!",[53,360,362],{"class":361},"sne4z","doctype",[53,364,365],{"class":70}," html",[53,367,368],{"class":77},">\n",[53,370,371,374,376,379,381,384],{"class":55,"line":63},[53,372,373],{"class":77},"\u003C",[53,375,351],{"class":361},[53,377,378],{"class":70}," lang",[53,380,170],{"class":77},[53,382,383],{"class":88},"\"en\"",[53,385,368],{"class":77},[53,387,388,390,393],{"class":55,"line":95},[53,389,373],{"class":77},[53,391,392],{"class":361},"head",[53,394,368],{"class":77},[53,396,397,399,402,405,407,410],{"class":55,"line":102},[53,398,373],{"class":77},[53,400,401],{"class":361},"meta",[53,403,404],{"class":70}," charset",[53,406,170],{"class":77},[53,408,409],{"class":88},"\"utf-8\"",[53,411,368],{"class":77},[53,413,414,416,418,421,423,426,429,431,434],{"class":55,"line":125},[53,415,373],{"class":77},[53,417,401],{"class":361},[53,419,420],{"class":70}," name",[53,422,170],{"class":77},[53,424,425],{"class":88},"\"viewport\"",[53,427,428],{"class":70}," content",[53,430,170],{"class":77},[53,432,433],{"class":88},"\"width=device-width,initial-scale=1\"",[53,435,368],{"class":77},[53,437,438,440,442,445,447],{"class":55,"line":145},[53,439,373],{"class":77},[53,441,234],{"class":361},[53,443,444],{"class":77},">Reading progress\u003C\u002F",[53,446,234],{"class":361},[53,448,368],{"class":77},[53,450,451,453,456],{"class":55,"line":164},[53,452,373],{"class":77},[53,454,455],{"class":361},"style",[53,457,368],{"class":77},[53,459,460,463,466,469,472,475,478,481,483,486,489,491,494,497,500,503],{"class":55,"line":185},[53,461,462],{"class":361},"  body",[53,464,465],{"class":77}," { ",[53,467,468],{"class":70},"margin",[53,470,471],{"class":77},": ",[53,473,474],{"class":70},"0",[53,476,477],{"class":77},"; ",[53,479,480],{"class":70},"font",[53,482,471],{"class":77},[53,484,485],{"class":70},"16",[53,487,488],{"class":66},"px",[53,490,158],{"class":77},[53,492,493],{"class":70},"1.6",[53,495,496],{"class":70}," system-ui",[53,498,499],{"class":77},", ",[53,501,502],{"class":70},"sans-serif",[53,504,505],{"class":77},"; }\n",[53,507,509],{"class":55,"line":508},9,[53,510,99],{"emptyLinePlaceholder":98},[53,512,514,517],{"class":55,"line":513},10,[53,515,516],{"class":70},"  .progress",[53,518,122],{"class":77},[53,520,522,525,527,530],{"class":55,"line":521},11,[53,523,524],{"class":70},"    position",[53,526,471],{"class":77},[53,528,529],{"class":70},"fixed",[53,531,182],{"class":77},[53,533,535,538,540,542],{"class":55,"line":534},12,[53,536,537],{"class":70},"    inset-block-start",[53,539,471],{"class":77},[53,541,474],{"class":70},[53,543,182],{"class":77},[53,545,547,550,552,554],{"class":55,"line":546},13,[53,548,549],{"class":70},"    inset-inline",[53,551,471],{"class":77},[53,553,474],{"class":70},[53,555,182],{"class":77},[53,557,559,562,564,567],{"class":55,"line":558},14,[53,560,561],{"class":70},"    z-index",[53,563,471],{"class":77},[53,565,566],{"class":70},"3",[53,568,182],{"class":77},[53,570,572,575,577,580,582],{"class":55,"line":571},15,[53,573,574],{"class":70},"    height",[53,576,471],{"class":77},[53,578,579],{"class":70},"5",[53,581,488],{"class":66},[53,583,182],{"class":77},[53,585,587,590,592,595],{"class":55,"line":586},16,[53,588,589],{"class":70},"    background",[53,591,471],{"class":77},[53,593,594],{"class":70},"#4361c8",[53,596,182],{"class":77},[53,598,600],{"class":55,"line":599},17,[53,601,602],{"class":59},"    \u002F* Scale from the left edge, so scaleX(0.4) fills the first 40%. *\u002F\n",[53,604,606,609,611,613,616,619],{"class":55,"line":605},18,[53,607,608],{"class":70},"    transform-origin",[53,610,471],{"class":77},[53,612,474],{"class":70},[53,614,615],{"class":70}," 50",[53,617,618],{"class":66},"%",[53,620,182],{"class":77},[53,622,624],{"class":55,"line":623},19,[53,625,626],{"class":59},"    \u002F* `linear` is mandatory: any easing curve would distort the mapping\n",[53,628,630],{"class":55,"line":629},20,[53,631,632],{"class":59},"       from scroll position onto animation progress. *\u002F\n",[53,634,636,639,642,645],{"class":55,"line":635},21,[53,637,638],{"class":70},"    animation",[53,640,641],{"class":77},": grow-progress ",[53,643,644],{"class":70},"linear",[53,646,182],{"class":77},[53,648,650],{"class":55,"line":649},22,[53,651,652],{"class":59},"    \u002F* `root` = the document's own scroller (not the nearest ancestor).\n",[53,654,656],{"class":55,"line":655},23,[53,657,658],{"class":59},"       `block` = the document's block axis, vertical in this writing mode. *\u002F\n",[53,660,662,665,667,669,672,675],{"class":55,"line":661},24,[53,663,664],{"class":70},"    animation-timeline",[53,666,471],{"class":77},[53,668,197],{"class":70},[53,670,671],{"class":77},"(root ",[53,673,674],{"class":70},"block",[53,676,92],{"class":77},[53,678,680],{"class":55,"line":679},25,[53,681,682],{"class":77},"  }\n",[53,684,686],{"class":55,"line":685},26,[53,687,99],{"emptyLinePlaceholder":98},[53,689,691,694,698],{"class":55,"line":690},27,[53,692,693],{"class":66},"  @keyframes",[53,695,697],{"class":696},"soyes"," grow-progress",[53,699,122],{"class":77},[53,701,703,706,708,711,713,716,718,720],{"class":55,"line":702},28,[53,704,705],{"class":70},"    from",[53,707,465],{"class":77},[53,709,710],{"class":70},"transform",[53,712,471],{"class":77},[53,714,715],{"class":70},"scaleX",[53,717,85],{"class":77},[53,719,474],{"class":70},[53,721,722],{"class":77},"); }\n",[53,724,726,729,732,734,736,738,740,743],{"class":55,"line":725},29,[53,727,728],{"class":70},"    to",[53,730,731],{"class":77},"   { ",[53,733,710],{"class":70},[53,735,471],{"class":77},[53,737,715],{"class":70},[53,739,85],{"class":77},[53,741,742],{"class":70},"1",[53,744,722],{"class":77},[53,746,748],{"class":55,"line":747},30,[53,749,682],{"class":77},[53,751,753],{"class":55,"line":752},31,[53,754,99],{"emptyLinePlaceholder":98},[53,756,758],{"class":55,"line":757},32,[53,759,760],{"class":59},"  \u002F* The bar carries position information, so it must never disappear.\n",[53,762,764],{"class":55,"line":763},33,[53,765,766],{"class":59},"     Under reduced motion we keep it and drop only the decorative colour\n",[53,768,770],{"class":55,"line":769},34,[53,771,772],{"class":59},"     travel; the fill itself is not a vestibular risk at 5px tall. *\u002F\n",[53,774,776,779],{"class":55,"line":775},35,[53,777,778],{"class":66},"  @media",[53,780,781],{"class":77}," (prefers-reduced-motion: reduce) {\n",[53,783,785,788,790,793,795,797],{"class":55,"line":784},36,[53,786,787],{"class":70},"    .progress",[53,789,465],{"class":77},[53,791,792],{"class":70},"background",[53,794,471],{"class":77},[53,796,594],{"class":70},[53,798,505],{"class":77},[53,800,802],{"class":55,"line":801},37,[53,803,682],{"class":77},[53,805,807],{"class":55,"line":806},38,[53,808,99],{"emptyLinePlaceholder":98},[53,810,812,815,817,820,822,825,828,830,832,834,836,839,841,844,846,849,851,854,856,859,862],{"class":55,"line":811},39,[53,813,814],{"class":70},"  .article",[53,816,465],{"class":77},[53,818,819],{"class":70},"max-width",[53,821,471],{"class":77},[53,823,824],{"class":70},"40",[53,826,827],{"class":66},"rem",[53,829,477],{"class":77},[53,831,468],{"class":70},[53,833,471],{"class":77},[53,835,474],{"class":70},[53,837,838],{"class":70}," auto",[53,840,477],{"class":77},[53,842,843],{"class":70},"padding",[53,845,471],{"class":77},[53,847,848],{"class":70},"2",[53,850,827],{"class":66},[53,852,853],{"class":70}," 1.25",[53,855,827],{"class":66},[53,857,858],{"class":70}," 60",[53,860,861],{"class":66},"vh",[53,863,505],{"class":77},[53,865,867,870,872],{"class":55,"line":866},40,[53,868,869],{"class":77},"\u003C\u002F",[53,871,455],{"class":361},[53,873,368],{"class":77},[53,875,877,879,881],{"class":55,"line":876},41,[53,878,869],{"class":77},[53,880,392],{"class":361},[53,882,368],{"class":77},[53,884,886,888,891],{"class":55,"line":885},42,[53,887,373],{"class":77},[53,889,890],{"class":361},"body",[53,892,368],{"class":77},[53,894,896],{"class":55,"line":895},43,[53,897,898],{"class":59},"  \u003C!-- role=\"presentation\": the bar duplicates information the scrollbar\n",[53,900,902],{"class":55,"line":901},44,[53,903,904],{"class":59},"       already exposes to assistive technology. Do not announce it twice. -->\n",[53,906,908,911,914,917,919,922,925,927,930,933,935],{"class":55,"line":907},45,[53,909,910],{"class":77},"  \u003C",[53,912,913],{"class":361},"div",[53,915,916],{"class":70}," class",[53,918,170],{"class":77},[53,920,921],{"class":88},"\"progress\"",[53,923,924],{"class":70}," role",[53,926,170],{"class":77},[53,928,929],{"class":88},"\"presentation\"",[53,931,932],{"class":77},">\u003C\u002F",[53,934,913],{"class":361},[53,936,368],{"class":77},[53,938,940],{"class":55,"line":939},46,[53,941,99],{"emptyLinePlaceholder":98},[53,943,945,947,950,952,954,957],{"class":55,"line":944},47,[53,946,910],{"class":77},[53,948,949],{"class":361},"article",[53,951,916],{"class":70},[53,953,170],{"class":77},[53,955,956],{"class":88},"\"article\"",[53,958,368],{"class":77},[53,960,962,965,967,970,972],{"class":55,"line":961},48,[53,963,964],{"class":77},"    \u003C",[53,966,10],{"class":361},[53,968,969],{"class":77},">The Compositor Never Blinks\u003C\u002F",[53,971,10],{"class":361},[53,973,368],{"class":77},[53,975,977,979,981,984,986],{"class":55,"line":976},49,[53,978,964],{"class":77},[53,980,14],{"class":361},[53,982,983],{"class":77},">Scroll this document and watch the bar at the top.\u003C\u002F",[53,985,14],{"class":361},[53,987,368],{"class":77},[53,989,991,993,995,998,1000],{"class":55,"line":990},50,[53,992,964],{"class":77},[53,994,14],{"class":361},[53,996,997],{"class":77},">Its progress is not being computed by any script.\u003C\u002F",[53,999,14],{"class":361},[53,1001,368],{"class":77},[53,1003,1005,1007,1009,1012,1014],{"class":55,"line":1004},51,[53,1006,964],{"class":77},[53,1008,14],{"class":361},[53,1010,1011],{"class":77},">It is a keyframe animation whose timeline is the scroller itself.\u003C\u002F",[53,1013,14],{"class":361},[53,1015,368],{"class":77},[53,1017,1019,1021,1023],{"class":55,"line":1018},52,[53,1020,964],{"class":77},[53,1022,14],{"class":361},[53,1024,1025],{"class":77},">Because the compositor already owns the scroll offset, sampling that\n",[53,1027,1029,1032,1034],{"class":55,"line":1028},53,[53,1030,1031],{"class":77},"       timeline costs nothing on the main thread.\u003C\u002F",[53,1033,14],{"class":361},[53,1035,368],{"class":77},[53,1037,1039,1041,1043,1046,1048],{"class":55,"line":1038},54,[53,1040,964],{"class":77},[53,1042,14],{"class":361},[53,1044,1045],{"class":77},">Block the main thread with a long task and the bar keeps pace.\u003C\u002F",[53,1047,14],{"class":361},[53,1049,368],{"class":77},[53,1051,1053,1055,1057,1060,1062],{"class":55,"line":1052},55,[53,1054,964],{"class":77},[53,1056,14],{"class":361},[53,1058,1059],{"class":77},">A scroll listener would freeze.\u003C\u002F",[53,1061,14],{"class":361},[53,1063,368],{"class":77},[53,1065,1067,1070,1072],{"class":55,"line":1066},56,[53,1068,1069],{"class":77},"  \u003C\u002F",[53,1071,949],{"class":361},[53,1073,368],{"class":77},[53,1075,1077,1079,1081],{"class":55,"line":1076},57,[53,1078,869],{"class":77},[53,1080,890],{"class":361},[53,1082,368],{"class":77},[53,1084,1086,1088,1090],{"class":55,"line":1085},58,[53,1087,869],{"class":77},[53,1089,351],{"class":361},[53,1091,368],{"class":77},[14,1093,1094,1095,1098,1099,1102,1103,1105],{},"The whole effect is four declarations on ",[18,1096,1097],{},".progress",", and only two of them are new: ",[18,1100,1101],{},"animation-timeline"," and the ",[18,1104,644],{}," in the shorthand.",[33,1107],{},[36,1109,1111],{"id":1110},"the-key-technique-a-timeline-with-no-duration","The Key Technique: A Timeline With No Duration",[14,1113,1114,1115,1118,1119,1122,1123,1126,1127,1130,1131,1134,1135,1138],{},"The move that makes this work is that ",[18,1116,1117],{},"animation-timeline: scroll(root block)"," replaces the animation's ",[200,1120,1121],{},"source of progress",". On the default document timeline, progress is ",[18,1124,1125],{},"elapsed \u002F duration",". On a scroll progress timeline, progress is ",[18,1128,1129],{},"currentScrollOffset \u002F maxScrollOffset",", evaluated fresh in whatever frame the scroll is applied. ",[18,1132,1133],{},"animation-duration"," becomes meaningless — you can declare ",[18,1136,1137],{},"4s"," or omit it entirely and the bar behaves identically — because nothing is measuring time.",[14,1140,1141],{},"That reframing is why the bar cannot fall behind. There is no callback to be scheduled and no value to be pushed; there is a function of scroll offset that the compositor evaluates while it is already handling the scroll. Progress and paint happen in the same frame, by construction.",[14,1143,1144,1145,1148,1149,1152,1153,1155],{},"The corollary is the one thing that trips people up: an easing function is now a ",[200,1146,1147],{},"distortion",", not a flourish. ",[18,1150,1151],{},"animation-timing-function: ease"," remaps progress non-linearly, so at 50% scroll the bar would read something other than 50% — the bar would race ahead early and crawl at the end. Always write ",[18,1154,644],{},". If you want a non-uniform bar (a fill that accelerates once the reader is past the introduction, say), express that with keyframe stops, which are honest about being a deliberate mapping:",[44,1157,1161],{"className":1158,"code":1159,"language":1160,"meta":49,"style":49},"language-css shiki shiki-themes github-light-high-contrast github-dark-high-contrast","@keyframes grow-progress {\n  from            { transform: scaleX(0); }\n  30%             { transform: scaleX(0.15); }\n  to              { transform: scaleX(1); }\n}\n","css",[18,1162,1163,1171,1191,1212,1232],{"__ignoreMap":49},[53,1164,1165,1167,1169],{"class":55,"line":56},[53,1166,20],{"class":66},[53,1168,697],{"class":696},[53,1170,122],{"class":77},[53,1172,1173,1176,1179,1181,1183,1185,1187,1189],{"class":55,"line":63},[53,1174,1175],{"class":70},"  from",[53,1177,1178],{"class":77},"            { ",[53,1180,710],{"class":70},[53,1182,471],{"class":77},[53,1184,715],{"class":70},[53,1186,85],{"class":77},[53,1188,474],{"class":70},[53,1190,722],{"class":77},[53,1192,1193,1196,1199,1201,1203,1205,1207,1210],{"class":55,"line":95},[53,1194,1195],{"class":70},"  30%",[53,1197,1198],{"class":77},"             { ",[53,1200,710],{"class":70},[53,1202,471],{"class":77},[53,1204,715],{"class":70},[53,1206,85],{"class":77},[53,1208,1209],{"class":70},"0.15",[53,1211,722],{"class":77},[53,1213,1214,1217,1220,1222,1224,1226,1228,1230],{"class":55,"line":102},[53,1215,1216],{"class":70},"  to",[53,1218,1219],{"class":77},"              { ",[53,1221,710],{"class":70},[53,1223,471],{"class":77},[53,1225,715],{"class":70},[53,1227,85],{"class":77},[53,1229,742],{"class":70},[53,1231,722],{"class":77},[53,1233,1234],{"class":55,"line":125},[53,1235,1236],{"class":77},"}\n",[33,1238],{},[36,1240,1242],{"id":1241},"variation-a-bar-scoped-to-the-article-not-the-document","Variation: A Bar Scoped to the Article, Not the Document",[14,1244,1245,1248,1249,1251],{},[18,1246,1247],{},"scroll(root)"," measures the whole document, so the bar hits 100% only at the very bottom of the page — past the footer, the related links, and the comment form. Readers experience that as a bar that never quite finishes. To measure the ",[200,1250,949],{}," instead, name a view timeline on the article element and bind the bar to that name.",[44,1253,1255],{"className":1158,"code":1254,"language":1160,"meta":49,"style":49},".article {\n  \u002F* The article's own pass through the scrollport becomes a named timeline. *\u002F\n  view-timeline-name: --article-pass;\n  view-timeline-axis: block;\n}\n\n.progress {\n  position: fixed;\n  inset-block-start: 0;\n  inset-inline: 0;\n  height: 5px;\n  background: #4361c8;\n  transform-origin: 0 50%;\n  animation: grow-progress linear;\n  \u002F* Reference the name instead of an anonymous scroll() timeline. *\u002F\n  animation-timeline: --article-pass;\n  \u002F* Start when the article's top edge reaches the top of the scrollport;\n     finish when its bottom edge does. This is the \"reading\" window. *\u002F\n  animation-range: contain 0% contain 100%;\n}\n",[18,1256,1257,1264,1269,1277,1288,1292,1296,1302,1313,1324,1335,1348,1359,1374,1385,1390,1397,1402,1407,1432],{"__ignoreMap":49},[53,1258,1259,1262],{"class":55,"line":56},[53,1260,1261],{"class":70},".article",[53,1263,122],{"class":77},[53,1265,1266],{"class":55,"line":63},[53,1267,1268],{"class":59},"  \u002F* The article's own pass through the scrollport becomes a named timeline. *\u002F\n",[53,1270,1271,1274],{"class":55,"line":95},[53,1272,1273],{"class":70},"  view-timeline-name",[53,1275,1276],{"class":77},": --article-pass;\n",[53,1278,1279,1282,1284,1286],{"class":55,"line":102},[53,1280,1281],{"class":70},"  view-timeline-axis",[53,1283,471],{"class":77},[53,1285,674],{"class":70},[53,1287,182],{"class":77},[53,1289,1290],{"class":55,"line":125},[53,1291,1236],{"class":77},[53,1293,1294],{"class":55,"line":145},[53,1295,99],{"emptyLinePlaceholder":98},[53,1297,1298,1300],{"class":55,"line":164},[53,1299,1097],{"class":70},[53,1301,122],{"class":77},[53,1303,1304,1307,1309,1311],{"class":55,"line":185},[53,1305,1306],{"class":70},"  position",[53,1308,471],{"class":77},[53,1310,529],{"class":70},[53,1312,182],{"class":77},[53,1314,1315,1318,1320,1322],{"class":55,"line":508},[53,1316,1317],{"class":70},"  inset-block-start",[53,1319,471],{"class":77},[53,1321,474],{"class":70},[53,1323,182],{"class":77},[53,1325,1326,1329,1331,1333],{"class":55,"line":513},[53,1327,1328],{"class":70},"  inset-inline",[53,1330,471],{"class":77},[53,1332,474],{"class":70},[53,1334,182],{"class":77},[53,1336,1337,1340,1342,1344,1346],{"class":55,"line":521},[53,1338,1339],{"class":70},"  height",[53,1341,471],{"class":77},[53,1343,579],{"class":70},[53,1345,488],{"class":66},[53,1347,182],{"class":77},[53,1349,1350,1353,1355,1357],{"class":55,"line":534},[53,1351,1352],{"class":70},"  background",[53,1354,471],{"class":77},[53,1356,594],{"class":70},[53,1358,182],{"class":77},[53,1360,1361,1364,1366,1368,1370,1372],{"class":55,"line":546},[53,1362,1363],{"class":70},"  transform-origin",[53,1365,471],{"class":77},[53,1367,474],{"class":70},[53,1369,615],{"class":70},[53,1371,618],{"class":66},[53,1373,182],{"class":77},[53,1375,1376,1379,1381,1383],{"class":55,"line":558},[53,1377,1378],{"class":70},"  animation",[53,1380,641],{"class":77},[53,1382,644],{"class":70},[53,1384,182],{"class":77},[53,1386,1387],{"class":55,"line":571},[53,1388,1389],{"class":59},"  \u002F* Reference the name instead of an anonymous scroll() timeline. *\u002F\n",[53,1391,1392,1395],{"class":55,"line":586},[53,1393,1394],{"class":70},"  animation-timeline",[53,1396,1276],{"class":77},[53,1398,1399],{"class":55,"line":599},[53,1400,1401],{"class":59},"  \u002F* Start when the article's top edge reaches the top of the scrollport;\n",[53,1403,1404],{"class":55,"line":605},[53,1405,1406],{"class":59},"     finish when its bottom edge does. This is the \"reading\" window. *\u002F\n",[53,1408,1409,1412,1414,1417,1420,1422,1425,1428,1430],{"class":55,"line":623},[53,1410,1411],{"class":70},"  animation-range",[53,1413,471],{"class":77},[53,1415,1416],{"class":70},"contain",[53,1418,1419],{"class":70}," 0",[53,1421,618],{"class":66},[53,1423,1424],{"class":70}," contain",[53,1426,1427],{"class":70}," 100",[53,1429,618],{"class":66},[53,1431,182],{"class":77},[53,1433,1434],{"class":55,"line":629},[53,1435,1236],{"class":77},[14,1437,1438,1439,1441,1442,1444,1445,1447,1448,1451],{},"The ",[18,1440,1416],{}," range is the important half of this variation. It spans the period during which the article fully occupies the scrollport, so the bar reads 0% exactly when the article's first line reaches the top and 100% exactly when its last line does — which is what \"progress through the article\" actually means to a reader. Because ",[18,1443,1097],{}," is a fixed-position element outside ",[18,1446,1261],{},", the name must be visible to it; if the two are not in the same subtree, hoist the name with ",[18,1449,1450],{},"timeline-scope: --article-pass"," on their common ancestor.",[33,1453],{},[36,1455,1457],{"id":1456},"browser-support","Browser Support",[14,1459,1460,1462,1463,1466,1467,1470,1471,1474],{},[18,1461,1101],{}," with the ",[18,1464,1465],{},"scroll()"," function shipped in Chrome and Edge 115 and in Safari 26; Firefox has the implementation but keeps it behind a preference as of mid-2026, so treat it as unsupported. The graceful outcome without a guard is that the bar sits at ",[18,1468,1469],{},"scaleX(1)",", permanently full, because the animation runs on the document timeline with a zero duration and jumps straight to the ",[18,1472,1473],{},"to"," keyframe — misleading rather than merely absent. Guard it so the failure is a bar that never appears:",[44,1476,1478],{"className":1158,"code":1477,"language":1160,"meta":49,"style":49},".progress { display: none; }\n\n@supports (animation-timeline: scroll()) {\n  .progress {\n    display: block;\n    animation: grow-progress linear;\n    animation-timeline: scroll(root block);\n  }\n}\n",[18,1479,1480,1495,1499,1516,1522,1533,1543,1557,1561],{"__ignoreMap":49},[53,1481,1482,1484,1486,1489,1491,1493],{"class":55,"line":56},[53,1483,1097],{"class":70},[53,1485,465],{"class":77},[53,1487,1488],{"class":70},"display",[53,1490,471],{"class":77},[53,1492,267],{"class":70},[53,1494,505],{"class":77},[53,1496,1497],{"class":55,"line":63},[53,1498,99],{"emptyLinePlaceholder":98},[53,1500,1501,1504,1507,1509,1511,1513],{"class":55,"line":95},[53,1502,1503],{"class":66},"@supports",[53,1505,1506],{"class":77}," (",[53,1508,1101],{"class":70},[53,1510,471],{"class":77},[53,1512,197],{"class":70},[53,1514,1515],{"class":77},"()) {\n",[53,1517,1518,1520],{"class":55,"line":102},[53,1519,516],{"class":70},[53,1521,122],{"class":77},[53,1523,1524,1527,1529,1531],{"class":55,"line":125},[53,1525,1526],{"class":70},"    display",[53,1528,471],{"class":77},[53,1530,674],{"class":70},[53,1532,182],{"class":77},[53,1534,1535,1537,1539,1541],{"class":55,"line":145},[53,1536,638],{"class":70},[53,1538,641],{"class":77},[53,1540,644],{"class":70},[53,1542,182],{"class":77},[53,1544,1545,1547,1549,1551,1553,1555],{"class":55,"line":164},[53,1546,664],{"class":70},[53,1548,471],{"class":77},[53,1550,197],{"class":70},[53,1552,671],{"class":77},[53,1554,674],{"class":70},[53,1556,92],{"class":77},[53,1558,1559],{"class":55,"line":185},[53,1560,682],{"class":77},[53,1562,1563],{"class":55,"line":508},[53,1564,1236],{"class":77},[14,1566,1567,1568,1572],{},"Do not reach for a JavaScript fallback that reproduces the effect for Firefox. Reinstating the listener puts back every cost this page exists to avoid, for a decorative strip; the full argument for choosing between enhancement and parity is in ",[27,1569,1571],{"href":1570},"\u002Fcss-only-micro-interactions-animations\u002Fscroll-driven-animations\u002Fscroll-driven-animation-fallbacks\u002F","scroll-driven animation fallbacks",".",[33,1574],{},[36,1576,1578],{"id":1577},"faq","FAQ",[14,1580,1581,1585,1586,1588],{},[1582,1583,1584],"strong",{},"Why animate scaleX instead of width for a progress bar?","\nWidth changes force the browser to lay the element out again on every frame, which pulls the work back onto the main thread. A ",[18,1587,715],{}," transform is handled entirely by the compositor, so the bar can keep pace with a fast scroll.",[14,1590,1591,1594,1595,1598,1599,1601],{},[1582,1592,1593],{},"What does scroll(root block) mean exactly?","\nThe first keyword picks the scroll container: ",[18,1596,1597],{},"root"," means the document's viewport scroller rather than the nearest ancestor scroller. The second picks the axis: ",[18,1600,674],{}," is the document's block direction, which is vertical in a horizontal writing mode.",[14,1603,1604,1607],{},[1582,1605,1606],{},"Should a progress bar be hidden under prefers-reduced-motion?","\nNo. The bar conveys position information, so removing it removes information. Keep it visible and keep it updating, and only drop any decorative extras such as a colour shift or a glow that travels along it.",[14,1609,1610,1613],{},[1582,1611,1612],{},"Does the bar reach 100 percent at the last paragraph?","\nNo, it reaches 100 percent at the scroller's maximum scroll offset, which is after the footer. If you want it full at the end of the article, put the article in its own scroll container or accept the offset.",[33,1615],{},[36,1617,1619],{"id":1618},"related","Related",[1621,1622,1623,1633,1643,1656,1663],"ul",{},[1624,1625,1626,1629,1630,1632],"li",{},[27,1627,1628],{"href":29},"Scroll-Driven Animations"," — the guide covering ",[18,1631,1101],{},", ranges, and named timelines in full.",[1624,1634,1635,1639,1640,1572],{},[27,1636,1638],{"href":1637},"\u002Fcss-only-micro-interactions-animations\u002Fscroll-driven-animations\u002Fscroll-triggered-reveal-animations\u002F","Scroll-triggered reveal animations"," — the per-element counterpart built on ",[18,1641,1642],{},"view()",[1624,1644,1645,1649,1650,210,1652,1655],{},[27,1646,1648],{"href":1647},"\u002Fcss-only-micro-interactions-animations\u002Fperformance-gpu-acceleration\u002Foptimizing-css-animations-for-60fps\u002F","Optimizing CSS animations for 60fps"," — why ",[18,1651,710],{},[18,1653,1654],{},"opacity"," are the only safe things to animate here.",[1624,1657,1658,1662],{},[27,1659,1661],{"href":1660},"\u002Fcss-only-micro-interactions-animations\u002Faccessibility-in-css-animations\u002Fprefers-reduced-motion-recipes\u002F","prefers-reduced-motion recipes"," — patterns for scaling motion back without deleting information.",[1624,1664,1665,1669],{},[27,1666,1668],{"href":1667},"\u002Fmastering-container-queries-responsive-layouts\u002Fcontainer-query-fallbacks\u002Fhandling-container-query-fallbacks-for-older-browsers\u002F","Handling container query fallbacks for older browsers"," — the same enhancement discipline applied on the layout side.",[455,1671,1672],{},"html pre.shiki code .sLBg1, html code.shiki .sLBg1{--shiki-default:#66707B;--shiki-dark:#BDC4CC}html pre.shiki code .sHUrx, html code.shiki .sHUrx{--shiki-default:#A0111F;--shiki-dark:#FF9492}html pre.shiki code .s-5SL, html code.shiki .s-5SL{--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .suds8, html code.shiki .suds8{--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .sKwhi, html code.shiki .sKwhi{--shiki-default:#622CBC;--shiki-dark:#DBB7FF}html pre.shiki code .sT6z2, html code.shiki .sT6z2{--shiki-default:#032563;--shiki-dark:#ADDCFF}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 pre.shiki code .sne4z, html code.shiki .sne4z{--shiki-default:#024C1A;--shiki-dark:#72F088}html pre.shiki code .soyes, html code.shiki .soyes{--shiki-default:#702C00;--shiki-dark:#FFB757}",{"title":49,"searchDepth":63,"depth":63,"links":1674},[1675,1676,1677,1678,1679,1680,1681],{"id":38,"depth":63,"text":39},{"id":338,"depth":63,"text":339},{"id":1110,"depth":63,"text":1111},{"id":1241,"depth":63,"text":1242},{"id":1456,"depth":63,"text":1457},{"id":1577,"depth":63,"text":1578},{"id":1618,"depth":63,"text":1619},"Build a reading-progress bar with animation-timeline: scroll(root) instead of a scroll listener. Complete runnable CSS, reduced motion, and support notes.","md",{"pageTitle":1685,"datePublished":1686,"dateModified":1686,"faq":1687},"Scroll Progress Bar Without JavaScript","2026-07-20",[1688,1690,1692,1694],{"q":1584,"a":1689},"Width changes force the browser to lay the element out again on every frame, which pulls the work back onto the main thread. A scaleX transform is handled entirely by the compositor, so the bar can keep pace with a fast scroll.",{"q":1593,"a":1691},"The first keyword picks the scroll container: root means the document's viewport scroller rather than the nearest ancestor scroller. The second picks the axis: block is the document's block direction, which is vertical in a horizontal writing mode.",{"q":1606,"a":1693},"No. The bar conveys position information, so removing it removes information. Keep it visible and keep it updating, and only drop any decorative extras such as a colour shift or a glow that travels along it.",{"q":1612,"a":1695},"No, it reaches 100 percent at the scroller's maximum scroll offset, which is after the footer. If you want it full at the end of the article, put the article in its own scroll container or accept the offset.","\u002Fcss-only-micro-interactions-animations\u002Fscroll-driven-animations\u002Fscroll-progress-bar-without-javascript",{"title":5,"description":1682},"css-only-micro-interactions-animations\u002Fscroll-driven-animations\u002Fscroll-progress-bar-without-javascript\u002Findex","oH3TETMEdEl3JLjjf-u1TsJt7x0M_5R0_9wkJ7wWDe0",[1701,1704,1707,1710,1713,1716,1719,1722,1725,1728,1731,1734,1737,1740,1743,1746,1749,1752,1755,1758,1761,1764,1767,1770,1773,1776,1779,1782,1785,1788,1791,1794,1797,1800,1803,1806,1809,1812,1815,1818,1821,1822,1825,1828,1831,1834,1837,1839,1842,1845,1848,1851,1854,1857,1860,1863,1866,1869,1872,1875,1878,1881,1884,1887,1890,1893,1896,1899,1902,1905,1908,1911,1914,1917,1920,1923,1926,1929,1932,1935,1938,1941,1944,1947,1950,1953,1956,1959],{"path":1702,"title":1703},"\u002Fcss-only-micro-interactions-animations\u002Faccessibility-in-css-animations\u002Fcreating-accessible-focus-indicators","Creating Accessible Focus Indicators: Building a Ring That Survives Any Background",{"path":1705,"title":1706},"\u002Fcss-only-micro-interactions-animations\u002Faccessibility-in-css-animations","Accessibility in CSS Animations: Patterns, Specs & Best Practices",{"path":1708,"title":1709},"\u002Fcss-only-micro-interactions-animations\u002Faccessibility-in-css-animations\u002Fprefers-reduced-motion-recipes","prefers-reduced-motion Recipes: A Reduction for Every Common Effect",{"path":1711,"title":1712},"\u002Fcss-only-micro-interactions-animations\u002Faccessibility-in-css-animations\u002Freducing-motion-preferences-in-css","Reducing Motion Preferences in CSS: How the Setting Becomes a Media Feature",{"path":1714,"title":1715},"\u002Fcss-only-micro-interactions-animations\u002Faccessibility-in-css-animations\u002Ftarget-size-and-pointer-accessibility","Target Size and Pointer Accessibility: Meeting 2.5.8 Without Redesigning",{"path":1717,"title":1718},"\u002Fcss-only-micro-interactions-animations\u002Faccessibility-in-css-animations\u002Fvestibular-safe-animation-patterns","Vestibular-Safe Animation Patterns: Why Some Motion Makes People Sick",{"path":1720,"title":1721},"\u002Fcss-only-micro-interactions-animations\u002Faccessibility-in-css-animations\u002Fwcag-motion-success-criteria","WCAG Motion Success Criteria: What Each One Actually Requires",{"path":1723,"title":1724},"\u002Fcss-only-micro-interactions-animations\u002Fcontainer-aware-motion\u002Fcontainer-query-hover-affordances","Container Query Hover Affordances: When Narrow Means Persistent, Not Hidden",{"path":1726,"title":1727},"\u002Fcss-only-micro-interactions-animations\u002Fcontainer-aware-motion","Container-Aware Motion: Animation Sized by the Space a Component Actually Has",{"path":1729,"title":1730},"\u002Fcss-only-micro-interactions-animations\u002Fcontainer-aware-motion\u002Fmotion-that-scales-with-container-size","Motion That Scales with Container Size: cqi, cqb, and Derived Durations",{"path":1732,"title":1733},"\u002Fcss-only-micro-interactions-animations\u002Fcontainer-aware-motion\u002Fsuppressing-motion-in-small-containers","Suppressing Motion in Small Containers: Deliberately Animating Nothing",{"path":1735,"title":1736},"\u002Fcss-only-micro-interactions-animations\u002Fcss-animation-vs-web-animations-api","CSS Animation vs the Web Animations API: A Decision Guide",{"path":1738,"title":1739},"\u002Fcss-only-micro-interactions-animations\u002Fcss-animation-vs-web-animations-api\u002Fwhen-to-use-javascript-animation","When to Use JavaScript Instead of CSS for Animation",{"path":1741,"title":1742},"\u002Fcss-only-micro-interactions-animations\u002Fcss-custom-properties-architecture\u002Fanimating-custom-properties-with-at-property","Animating Custom Properties with @property: Typed Variables That Interpolate",{"path":1744,"title":1745},"\u002Fcss-only-micro-interactions-animations\u002Fcss-custom-properties-architecture\u002Ffluid-spacing-tokens-driving-transition-durations","Fluid Spacing Tokens That Drive Transition Durations",{"path":1747,"title":1748},"\u002Fcss-only-micro-interactions-animations\u002Fcss-custom-properties-architecture","CSS Custom Properties Architecture: Scalable Design Systems & Dynamic UI",{"path":1750,"title":1751},"\u002Fcss-only-micro-interactions-animations\u002Fcss-custom-properties-architecture\u002Fregistered-properties-and-type-safety","Registered Properties and Type Safety: @property as a Contract for Design Tokens",{"path":1753,"title":1754},"\u002Fcss-only-micro-interactions-animations\u002Fcss-transition-fundamentals\u002Fcss-transition-timing-functions","CSS Transition Timing Functions: ease, cubic-bezier(), steps() and linear()",{"path":1756,"title":1757},"\u002Fcss-only-micro-interactions-animations\u002Fcss-transition-fundamentals","CSS Transition Fundamentals: Architecture, Performance & Patterns",{"path":1759,"title":1760},"\u002Fcss-only-micro-interactions-animations\u002Fcss-transition-fundamentals\u002Fstarting-style-entry-animations","@starting-style: Animating an Element's Very First Style Change",{"path":1762,"title":1763},"\u002Fcss-only-micro-interactions-animations\u002Fcss-transition-fundamentals\u002Ftransitioning-display-with-allow-discrete","Transitioning display with transition-behavior: allow-discrete and @starting-style",{"path":1765,"title":1766},"\u002Fcss-only-micro-interactions-animations\u002Fhover-focus-state-design\u002Faccessible-css-only-tooltips","Accessible CSS-Only Tooltips and Hover Cards With  and ",{"path":1768,"title":1769},"\u002Fcss-only-micro-interactions-animations\u002Fhover-focus-state-design\u002Ffocus-visible-vs-focus-polyfill-alternatives"," vs :focus: Why It Replaced the Focus-Ring Polyfill",{"path":1771,"title":1772},"\u002Fcss-only-micro-interactions-animations\u002Fhover-focus-state-design\u002Ffocus-within-form-patterns"," Form Patterns: Styling a Group From the Field Inside It",{"path":1774,"title":1775},"\u002Fcss-only-micro-interactions-animations\u002Fhover-focus-state-design","Hover & Focus State Design: Spec-Compliant Patterns for Modern UIs",{"path":1777,"title":1778},"\u002Fcss-only-micro-interactions-animations\u002Fhover-focus-state-design\u002Fpointer-and-hover-media-queries","Pointer and Hover Media Queries: Gating Affordances by Input Capability",{"path":1780,"title":1781},"\u002Fcss-only-micro-interactions-animations\u002Fhover-focus-state-design\u002Fsmooth-hover-effects-without-javascript","Smooth Hover Effects Without JavaScript: CSS-Only Patterns for Modern UI",{"path":1783,"title":1784},"\u002Fcss-only-micro-interactions-animations","CSS-Only Micro-Interactions & Animations: Architecture, Performance & Implementation",{"path":1786,"title":1787},"\u002Fcss-only-micro-interactions-animations\u002Fkeyframe-animation-patterns\u002Fcontainer-query-triggered-keyframe-animations","Container-Query-Triggered Keyframe Animations: Animate Differently per Available Space",{"path":1789,"title":1790},"\u002Fcss-only-micro-interactions-animations\u002Fkeyframe-animation-patterns\u002Fcss-only-accordions-and-disclosure","CSS-Only Accordions and Disclosure Widgets with details and summary",{"path":1792,"title":1793},"\u002Fcss-only-micro-interactions-animations\u002Fkeyframe-animation-patterns\u002Fcss-only-loading-spinners-and-skeletons","CSS-Only Loading Spinners and Skeleton Shimmer Screens with @keyframes",{"path":1795,"title":1796},"\u002Fcss-only-micro-interactions-animations\u002Fkeyframe-animation-patterns\u002Fcss-only-toggle-switches-and-checkboxes","CSS-Only Toggle Switches and Checkboxes That Keep the Native Input",{"path":1798,"title":1799},"\u002Fcss-only-micro-interactions-animations\u002Fkeyframe-animation-patterns","Keyframe Animation Patterns: Spec-Compliant Architectures for Modern UIs",{"path":1801,"title":1802},"\u002Fcss-only-micro-interactions-animations\u002Fkeyframe-animation-patterns\u002Fstaggered-list-animations-with-custom-properties","Staggered List Animations Using --i Index Custom Properties",{"path":1804,"title":1805},"\u002Fcss-only-micro-interactions-animations\u002Fperformance-gpu-acceleration","Performance & GPU Acceleration in CSS: A Developer’s Blueprint",{"path":1807,"title":1808},"\u002Fcss-only-micro-interactions-animations\u002Fperformance-gpu-acceleration\u002Foptimizing-css-animations-for-60fps","Optimizing CSS Animations for 60fps: Budgeting the Frame",{"path":1810,"title":1811},"\u002Fcss-only-micro-interactions-animations\u002Fperformance-gpu-acceleration\u002Fprofiling-animations-in-devtools","Profiling CSS Animations in DevTools: Finding the Frame That Costs Too Much",{"path":1813,"title":1814},"\u002Fcss-only-micro-interactions-animations\u002Fperformance-gpu-acceleration\u002Fwill-change-and-the-compositor-thread","will-change and the Compositor Thread: Layer Promotion Without the Footguns",{"path":1816,"title":1817},"\u002Fcss-only-micro-interactions-animations\u002Fscroll-driven-animations","Scroll-Driven Animations: Binding Motion to Scroll Position in Pure CSS",{"path":1819,"title":1820},"\u002Fcss-only-micro-interactions-animations\u002Fscroll-driven-animations\u002Fscroll-driven-animation-fallbacks","Shipping Scroll-Driven Animations Without Breaking Anything",{"path":1696,"title":5},{"path":1823,"title":1824},"\u002Fcss-only-micro-interactions-animations\u002Fscroll-driven-animations\u002Fscroll-triggered-reveal-animations","Reveal-on-Scroll with view() and animation-range",{"path":1826,"title":1827},"\u002Fcss-only-micro-interactions-animations\u002Fview-transitions-for-css-developers","View Transitions for CSS Developers: the Pseudo-Element Tree, Snapshots, and Named Elements",{"path":1829,"title":1830},"\u002Fcss-only-micro-interactions-animations\u002Fview-transitions-for-css-developers\u002Fsame-document-view-transitions","Writing a Same-Document View Transition: the CSS You Actually Author",{"path":1832,"title":1833},"\u002Fcss-only-micro-interactions-animations\u002Fview-transitions-for-css-developers\u002Fview-transition-names-shared-elements","view-transition-name and Shared Elements: Morphing a Thumbnail into a Hero",{"path":1835,"title":1836},"\u002Fcss-only-micro-interactions-animations\u002Fview-transitions-for-css-developers\u002Fview-transitions-with-reduced-motion","Honouring prefers-reduced-motion in View Transitions",{"path":158,"title":1838},"Modern CSS Layouts & Micro-Interactions",{"path":1840,"title":1841},"\u002Fmastering-container-queries-responsive-layouts\u002Fanchor-positioning-and-overlays\u002Fanchor-positioned-tooltips","Anchor-Positioned Tooltips: Tethering, Flipping, and Getting the Semantics Right",{"path":1843,"title":1844},"\u002Fmastering-container-queries-responsive-layouts\u002Fanchor-positioning-and-overlays\u002Fanchor-positioning-fallbacks","Anchor Positioning Fallbacks: A Graceful Floor for Older Browser Versions",{"path":1846,"title":1847},"\u002Fmastering-container-queries-responsive-layouts\u002Fanchor-positioning-and-overlays","CSS Anchor Positioning and Overlays: Tethering Elements Without JavaScript",{"path":1849,"title":1850},"\u002Fmastering-container-queries-responsive-layouts\u002Fanchor-positioning-and-overlays\u002Fpopover-attribute-and-css-styling","The popover Attribute: Top-Layer Overlays With No JavaScript",{"path":1852,"title":1853},"\u002Fmastering-container-queries-responsive-layouts\u002Fcontainer-query-fallbacks\u002Ffeature-detection-with-supports","Feature Detection with @supports: Syntax, Semantics, and the Traps",{"path":1855,"title":1856},"\u002Fmastering-container-queries-responsive-layouts\u002Fcontainer-query-fallbacks\u002Fhandling-container-query-fallbacks-for-older-browsers","Handling Container Query Fallbacks for Older Browsers",{"path":1858,"title":1859},"\u002Fmastering-container-queries-responsive-layouts\u002Fcontainer-query-fallbacks","Container Query Fallbacks: Spec-Compliant CSS Strategies for Legacy Browsers",{"path":1861,"title":1862},"\u002Fmastering-container-queries-responsive-layouts\u002Fcontainer-query-syntax-basics\u002Fcontainer-query-units-cqi-cqb-explained","Container Query Units Explained: cqi, cqb, cqw, cqh, cqmin and cqmax",{"path":1864,"title":1865},"\u002Fmastering-container-queries-responsive-layouts\u002Fcontainer-query-syntax-basics\u002Fcontainer-type-size-vs-inline-size","Choosing container-type: inline-size, size, or normal",{"path":1867,"title":1868},"\u002Fmastering-container-queries-responsive-layouts\u002Fcontainer-query-syntax-basics\u002Fcontainer-vs-media-queries-comparison","Container vs Media Queries: Choosing the Right Reference Box",{"path":1870,"title":1871},"\u002Fmastering-container-queries-responsive-layouts\u002Fcontainer-query-syntax-basics\u002Fhow-to-use-container-queries-in-production","How to Use Container Queries in Production Without a Rewrite",{"path":1873,"title":1874},"\u002Fmastering-container-queries-responsive-layouts\u002Fcontainer-query-syntax-basics","Container Query Syntax Basics",{"path":1876,"title":1877},"\u002Fmastering-container-queries-responsive-layouts\u002Fcontainer-query-syntax-basics\u002Fnesting-and-naming-container-queries","Nesting and Naming Container Queries: Targeting the Right Ancestor",{"path":1879,"title":1880},"\u002Fmastering-container-queries-responsive-layouts\u002Fcss-grid-and-subgrid-layouts\u002Fauto-fit-minmax-responsive-grids","Responsive Grids with repeat(auto-fit, minmax())",{"path":1882,"title":1883},"\u002Fmastering-container-queries-responsive-layouts\u002Fcss-grid-and-subgrid-layouts\u002Fbuilding-a-dashboard-with-subgrid","Building a Responsive Dashboard Where Cards Align with Subgrid",{"path":1885,"title":1886},"\u002Fmastering-container-queries-responsive-layouts\u002Fcss-grid-and-subgrid-layouts\u002Fholy-grail-layout-with-grid","The Holy-Grail Layout with CSS Grid and Container Queries",{"path":1888,"title":1889},"\u002Fmastering-container-queries-responsive-layouts\u002Fcss-grid-and-subgrid-layouts","CSS Grid and Subgrid Layouts for Responsive Interfaces",{"path":1891,"title":1892},"\u002Fmastering-container-queries-responsive-layouts\u002Fcss-grid-and-subgrid-layouts\u002Fsubgrid-vs-nested-grid","Subgrid vs an Independently Nested Grid",{"path":1894,"title":1895},"\u002Fmastering-container-queries-responsive-layouts\u002Ffluid-typography-with-clamp\u002Fclamp-vs-media-query-typography","clamp() Fluid Type vs Media-Query Step Typography: Tradeoffs and Anatomy",{"path":1897,"title":1898},"\u002Fmastering-container-queries-responsive-layouts\u002Ffluid-typography-with-clamp\u002Ffluid-space-scale-with-clamp","Building a Fluid Spacing Scale with clamp() and Container Units",{"path":1900,"title":1901},"\u002Fmastering-container-queries-responsive-layouts\u002Ffluid-typography-with-clamp\u002Ffluid-type-accessibility-and-zoom","Fluid Type, Accessibility and Zoom: The vw-Only Trap",{"path":1903,"title":1904},"\u002Fmastering-container-queries-responsive-layouts\u002Ffluid-typography-with-clamp\u002Ffluid-typography-without-javascript","Fluid Typography Without JavaScript: A Precision CSS Reference",{"path":1906,"title":1907},"\u002Fmastering-container-queries-responsive-layouts\u002Ffluid-typography-with-clamp","Fluid Typography with clamp(): A Practical Guide for Modern CSS",{"path":1909,"title":1910},"\u002Fmastering-container-queries-responsive-layouts","Mastering Container Queries & Responsive Layouts",{"path":1912,"title":1913},"\u002Fmastering-container-queries-responsive-layouts\u002Fintrinsic-sizing-techniques\u002Faspect-ratio-for-responsive-media","Using aspect-ratio for Responsive Media and Preventing Layout Shift",{"path":1915,"title":1916},"\u002Fmastering-container-queries-responsive-layouts\u002Fintrinsic-sizing-techniques","Intrinsic Sizing Techniques: Modern CSS Layouts for Responsive UI",{"path":1918,"title":1919},"\u002Fmastering-container-queries-responsive-layouts\u002Fintrinsic-sizing-techniques\u002Fmin-max-fit-content-explained","min-content, max-content, and fit-content() Explained",{"path":1921,"title":1922},"\u002Fmastering-container-queries-responsive-layouts\u002Fintrinsic-sizing-techniques\u002Fpreventing-flex-and-grid-overflow","The min-width: auto Trap in Flex and Grid Layouts",{"path":1924,"title":1925},"\u002Fmastering-container-queries-responsive-layouts\u002Fmodern-css-reset-strategies\u002Fcascade-layers-for-reset-and-tokens","Cascade Layers for Reset and Design Tokens",{"path":1927,"title":1928},"\u002Fmastering-container-queries-responsive-layouts\u002Fmodern-css-reset-strategies\u002Fcascade-layers-vs-specificity-hacks","Cascade Layers vs Specificity Hacks: Ordering Instead of Escalating",{"path":1930,"title":1931},"\u002Fmastering-container-queries-responsive-layouts\u002Fmodern-css-reset-strategies","Modern CSS Reset Strategies: A Spec-Compliant Foundation",{"path":1933,"title":1934},"\u002Fmastering-container-queries-responsive-layouts\u002Fresponsive-component-patterns\u002Fbuilding-responsive-cards-with-container-queries","Building Responsive Cards with Container Queries: A Production-Ready Guide",{"path":1936,"title":1937},"\u002Fmastering-container-queries-responsive-layouts\u002Fresponsive-component-patterns\u002Fcontainer-query-data-tables","Container Query Data Tables: Reflowing Tables to Cards",{"path":1939,"title":1940},"\u002Fmastering-container-queries-responsive-layouts\u002Fresponsive-component-patterns\u002Fcontainer-query-sidebar-layouts","Container Query Sidebar Layouts That Travel Between Contexts",{"path":1942,"title":1943},"\u002Fmastering-container-queries-responsive-layouts\u002Fresponsive-component-patterns","Responsive Component Patterns: Architecture & Implementation",{"path":1945,"title":1946},"\u002Fmastering-container-queries-responsive-layouts\u002Fresponsive-component-patterns\u002Fresponsive-forms-with-container-queries","Responsive Forms with Container Queries: Reflow Without Losing the Wiring",{"path":1948,"title":1949},"\u002Fmastering-container-queries-responsive-layouts\u002Fresponsive-component-patterns\u002Fresponsive-navigation-without-media-queries","Responsive Navigation Without Media Queries Using Container Queries",{"path":1951,"title":1952},"\u002Fmastering-container-queries-responsive-layouts\u002Fstyle-queries-and-container-state\u002Fcontainer-style-query-theming","Container Style Query Theming: One Token Instead of a Variant Class Matrix",{"path":1954,"title":1955},"\u002Fmastering-container-queries-responsive-layouts\u002Fstyle-queries-and-container-state","Style Queries and Container State: Token-Driven Component Variants",{"path":1957,"title":1958},"\u002Fmastering-container-queries-responsive-layouts\u002Fstyle-queries-and-container-state\u002Fstyle-queries-with-custom-properties","Style Queries With Custom Properties: Declaring, Inheriting, and Matching Tokens",{"path":1960,"title":1961},"\u002Fmastering-container-queries-responsive-layouts\u002Fstyle-queries-and-container-state\u002Fstyle-query-fallbacks-and-support","Style Query Fallbacks and Support: Designing for Older Browser Versions",1784566157224]