[{"data":1,"prerenderedAt":638},["ShallowReactive",2],{"post-\u002Fposts\u002Fcss-scroll-driven-animations":3,"surround-\u002Fposts\u002Fcss-scroll-driven-animations":629},{"id":4,"title":5,"body":6,"cover":616,"date":617,"description":618,"draft":619,"extension":620,"meta":621,"minutes":67,"navigation":212,"path":622,"seo":623,"stem":624,"tags":625,"updated":627,"__hash__":628},"posts\u002Fposts\u002Fcss-scroll-driven-animations.md","滚动驱动动画：让 scroll 监听器退休",{"type":7,"value":8,"toc":609},"minimark",[9,18,22,32,36,39,292,298,302,308,473,479,482,489,595,598,602,605],[10,11,12,13,17],"p",{},"很长一段时间里，「滚动到哪，动画走到哪」意味着 JS 监听 ",[14,15,16],"code",{},"scroll"," 事件、算百分比、改样式。这条路径每一步都在主线程上，稍有不慎就是掉帧。CSS 滚动驱动动画把这件事整个搬进了浏览器内部。",[19,20,21],"h2",{"id":21},"核心概念只有一个",[10,23,24,27,28,31],{},[14,25,26],{},"animation-timeline"," 允许把动画的时间轴从「时钟」换成「滚动位置」。动画本身还是普通的 ",[14,29,30],{},"@keyframes","，变的只是驱动它的东西。",[19,33,35],{"id":34},"例子一阅读进度条","例子一：阅读进度条",[10,37,38],{},"本站文章页顶部的进度条没有一行 JS：",[40,41,46],"pre",{"className":42,"code":43,"language":44,"meta":45,"style":45},"language-css shiki shiki-themes tokyo-night github-light",".reading-progress {\n  position: fixed;\n  inset: 0 0 auto 0;\n  height: 2px;\n  background: var(--accent);\n  transform-origin: 0 50%;\n  animation: grow linear both;\n  animation-timeline: scroll(root);\n}\n\n@keyframes grow {\n  from { transform: scaleX(0); }\n  to { transform: scaleX(1); }\n}\n","css","",[14,47,48,65,83,103,120,144,162,182,201,207,214,231,262,287],{"__ignoreMap":45},[49,50,53,57,61],"span",{"class":51,"line":52},"line",1,[49,54,56],{"class":55},"sUzPf",".",[49,58,60],{"class":59},"sgoI7","reading-progress",[49,62,64],{"class":63},"sjXpA"," {\n",[49,66,68,72,76,80],{"class":51,"line":67},2,[49,69,71],{"class":70},"suUSV","  position",[49,73,75],{"class":74},"szu1u",":",[49,77,79],{"class":78},"sjYRd"," fixed",[49,81,82],{"class":74},";\n",[49,84,86,89,91,94,96,99,101],{"class":51,"line":85},3,[49,87,88],{"class":70},"  inset",[49,90,75],{"class":74},[49,92,93],{"class":78}," 0",[49,95,93],{"class":78},[49,97,98],{"class":78}," auto",[49,100,93],{"class":78},[49,102,82],{"class":74},[49,104,106,109,111,114,118],{"class":51,"line":105},4,[49,107,108],{"class":70},"  height",[49,110,75],{"class":74},[49,112,113],{"class":78}," 2",[49,115,117],{"class":116},"s8ozE","px",[49,119,82],{"class":74},[49,121,123,126,128,132,135,139,142],{"class":51,"line":122},5,[49,124,125],{"class":70},"  background",[49,127,75],{"class":74},[49,129,131],{"class":130},"sZlli"," var",[49,133,134],{"class":63},"(",[49,136,138],{"class":137},"sAEpf","--accent",[49,140,141],{"class":63},")",[49,143,82],{"class":74},[49,145,147,150,152,154,157,160],{"class":51,"line":146},6,[49,148,149],{"class":70},"  transform-origin",[49,151,75],{"class":74},[49,153,93],{"class":78},[49,155,156],{"class":78}," 50",[49,158,159],{"class":116},"%",[49,161,82],{"class":74},[49,163,165,168,170,174,177,180],{"class":51,"line":164},7,[49,166,167],{"class":70},"  animation",[49,169,75],{"class":74},[49,171,173],{"class":172},"sHgQP"," grow ",[49,175,176],{"class":78},"linear",[49,178,179],{"class":78}," both",[49,181,82],{"class":74},[49,183,185,189,191,194,197,199],{"class":51,"line":184},8,[49,186,188],{"class":187},"sSdlY","  animation-timeline",[49,190,75],{"class":74},[49,192,193],{"class":78}," scroll",[49,195,196],{"class":172},"(root",[49,198,141],{"class":63},[49,200,82],{"class":74},[49,202,204],{"class":51,"line":203},9,[49,205,206],{"class":63},"}\n",[49,208,210],{"class":51,"line":209},10,[49,211,213],{"emptyLinePlaceholder":212},true,"\n",[49,215,217,221,225,229],{"class":51,"line":216},11,[49,218,220],{"class":219},"sy3HM","@",[49,222,224],{"class":223},"sBxw3","keyframes",[49,226,228],{"class":227},"sgjcI"," grow",[49,230,64],{"class":63},[49,232,234,238,241,244,246,249,251,254,256,259],{"class":51,"line":233},12,[49,235,237],{"class":236},"sXJ4V","  from",[49,239,240],{"class":63}," { ",[49,242,243],{"class":70},"transform",[49,245,75],{"class":74},[49,247,248],{"class":130}," scaleX",[49,250,134],{"class":63},[49,252,253],{"class":78},"0",[49,255,141],{"class":63},[49,257,258],{"class":74},";",[49,260,261],{"class":63}," }\n",[49,263,265,268,270,272,274,276,278,281,283,285],{"class":51,"line":264},13,[49,266,267],{"class":236},"  to",[49,269,240],{"class":63},[49,271,243],{"class":70},[49,273,75],{"class":74},[49,275,248],{"class":130},[49,277,134],{"class":63},[49,279,280],{"class":78},"1",[49,282,141],{"class":63},[49,284,258],{"class":74},[49,286,261],{"class":63},[49,288,290],{"class":51,"line":289},14,[49,291,206],{"class":63},[10,293,294,297],{},[14,295,296],{},"scroll(root)"," 表示时间轴来自根滚动容器。页面滚到底，动画正好播完。",[19,299,301],{"id":300},"例子二元素入场","例子二：元素入场",[10,303,304,307],{},[14,305,306],{},"view()"," 时间轴更进一步：动画进度由「元素在视口中的位置」决定，相当于内置了 IntersectionObserver 加补间：",[40,309,311],{"className":42,"code":310,"language":44,"meta":45,"style":45},".card {\n  animation: rise linear both;\n  animation-timeline: view();\n  animation-range: entry 0% entry 80%;\n}\n\n@keyframes rise {\n  from {\n    opacity: 0;\n    transform: translateY(24px);\n  }\n  to {\n    opacity: 1;\n    transform: none;\n  }\n}\n",[14,312,313,322,337,350,373,377,381,392,398,409,430,435,441,452,463,468],{"__ignoreMap":45},[49,314,315,317,320],{"class":51,"line":52},[49,316,56],{"class":55},[49,318,319],{"class":59},"card",[49,321,64],{"class":63},[49,323,324,326,328,331,333,335],{"class":51,"line":67},[49,325,167],{"class":70},[49,327,75],{"class":74},[49,329,330],{"class":172}," rise ",[49,332,176],{"class":78},[49,334,179],{"class":78},[49,336,82],{"class":74},[49,338,339,341,343,346,348],{"class":51,"line":85},[49,340,188],{"class":187},[49,342,75],{"class":74},[49,344,345],{"class":172}," view(",[49,347,141],{"class":63},[49,349,82],{"class":74},[49,351,352,355,357,360,362,364,366,369,371],{"class":51,"line":105},[49,353,354],{"class":187},"  animation-range",[49,356,75],{"class":74},[49,358,359],{"class":172}," entry ",[49,361,253],{"class":78},[49,363,159],{"class":116},[49,365,359],{"class":172},[49,367,368],{"class":78},"80",[49,370,159],{"class":116},[49,372,82],{"class":74},[49,374,375],{"class":51,"line":122},[49,376,206],{"class":63},[49,378,379],{"class":51,"line":146},[49,380,213],{"emptyLinePlaceholder":212},[49,382,383,385,387,390],{"class":51,"line":164},[49,384,220],{"class":219},[49,386,224],{"class":223},[49,388,389],{"class":227}," rise",[49,391,64],{"class":63},[49,393,394,396],{"class":51,"line":184},[49,395,237],{"class":236},[49,397,64],{"class":63},[49,399,400,403,405,407],{"class":51,"line":203},[49,401,402],{"class":70},"    opacity",[49,404,75],{"class":74},[49,406,93],{"class":78},[49,408,82],{"class":74},[49,410,411,414,416,419,421,424,426,428],{"class":51,"line":209},[49,412,413],{"class":70},"    transform",[49,415,75],{"class":74},[49,417,418],{"class":130}," translateY",[49,420,134],{"class":63},[49,422,423],{"class":78},"24",[49,425,117],{"class":116},[49,427,141],{"class":63},[49,429,82],{"class":74},[49,431,432],{"class":51,"line":216},[49,433,434],{"class":63},"  }\n",[49,436,437,439],{"class":51,"line":233},[49,438,267],{"class":236},[49,440,64],{"class":63},[49,442,443,445,447,450],{"class":51,"line":264},[49,444,402],{"class":70},[49,446,75],{"class":74},[49,448,449],{"class":78}," 1",[49,451,82],{"class":74},[49,453,454,456,458,461],{"class":51,"line":289},[49,455,413],{"class":70},[49,457,75],{"class":74},[49,459,460],{"class":78}," none",[49,462,82],{"class":74},[49,464,466],{"class":51,"line":465},15,[49,467,434],{"class":63},[49,469,471],{"class":51,"line":470},16,[49,472,206],{"class":63},[10,474,475,478],{},[14,476,477],{},"animation-range"," 限定动画只在元素进入视口的前 80% 行程内播放，之后保持最终状态。",[19,480,481],{"id":481},"渐进增强与可访问性",[10,483,484,485,488],{},"截至写作时，Chromium 系浏览器与 Safari 都已支持，Firefox 仍需手动开启旗标。所以要包一层 ",[14,486,487],{},"@supports","，让不支持的浏览器直接看到最终状态：",[40,490,492],{"className":42,"code":491,"language":44,"meta":45,"style":45},"@supports not (animation-timeline: scroll()) {\n  .reading-progress { display: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .card { animation: none; }\n}\n",[14,493,494,525,545,549,553,572,591],{"__ignoreMap":45},[49,495,496,498,501,504,507,510,512,514,517,519,522],{"class":51,"line":52},[49,497,220],{"class":219},[49,499,500],{"class":223},"supports",[49,502,503],{"class":223}," not",[49,505,506],{"class":74}," (",[49,508,26],{"class":509},"sF5CD",[49,511,75],{"class":74},[49,513,193],{"class":78},[49,515,134],{"class":516},"sGNLY",[49,518,141],{"class":74},[49,520,521],{"class":516},") ",[49,523,524],{"class":63},"{\n",[49,526,527,530,532,534,537,539,541,543],{"class":51,"line":67},[49,528,529],{"class":55},"  .",[49,531,60],{"class":59},[49,533,240],{"class":63},[49,535,536],{"class":70},"display",[49,538,75],{"class":74},[49,540,460],{"class":78},[49,542,258],{"class":74},[49,544,261],{"class":63},[49,546,547],{"class":51,"line":85},[49,548,206],{"class":63},[49,550,551],{"class":51,"line":105},[49,552,213],{"emptyLinePlaceholder":212},[49,554,555,558,560,563,565,568,570],{"class":51,"line":122},[49,556,557],{"class":219},"@media",[49,559,506],{"class":63},[49,561,562],{"class":516},"prefers-reduced-motion",[49,564,75],{"class":74},[49,566,567],{"class":516}," reduce",[49,569,141],{"class":63},[49,571,64],{"class":63},[49,573,574,576,578,580,583,585,587,589],{"class":51,"line":146},[49,575,529],{"class":55},[49,577,319],{"class":59},[49,579,240],{"class":63},[49,581,582],{"class":70},"animation",[49,584,75],{"class":74},[49,586,460],{"class":78},[49,588,258],{"class":74},[49,590,261],{"class":63},[49,592,593],{"class":51,"line":164},[49,594,206],{"class":63},[10,596,597],{},"进度条属于状态指示，隐藏即可；入场动效属于装饰，在用户声明减少动态时应当彻底关闭。",[19,599,601],{"id":600},"什么时候还需要-js","什么时候还需要 JS",[10,603,604],{},"需要根据滚动做逻辑判断（比如目录高亮当前章节）时，IntersectionObserver 依然是正确工具。原则很简单：表现层交给 CSS 时间轴，语义层交给观察者。",[606,607,608],"style",{},"html pre.shiki code .sUzPf, html code.shiki .sUzPf{--shiki-default:#E0AF68;--shiki-light:#6F42C1}html pre.shiki code .sgoI7, html code.shiki .sgoI7{--shiki-default:#9ECE6A;--shiki-light:#6F42C1}html pre.shiki code .sjXpA, html code.shiki .sjXpA{--shiki-default:#9ABDF5;--shiki-light:#24292E}html pre.shiki code .suUSV, html code.shiki .suUSV{--shiki-default:#7AA2F7;--shiki-light:#005CC5}html pre.shiki code .szu1u, html code.shiki .szu1u{--shiki-default:#89DDFF;--shiki-light:#24292E}html pre.shiki code .sjYRd, html code.shiki .sjYRd{--shiki-default:#FF9E64;--shiki-light:#005CC5}html pre.shiki code .s8ozE, html code.shiki .s8ozE{--shiki-default:#F7768E;--shiki-light:#D73A49}html pre.shiki code .sZlli, html code.shiki .sZlli{--shiki-default:#0DB9D7;--shiki-light:#005CC5}html pre.shiki code .sAEpf, html code.shiki .sAEpf{--shiki-default:#C0CAF5;--shiki-light:#E36209}html pre.shiki code .sHgQP, html code.shiki .sHgQP{--shiki-default:#BB9AF7;--shiki-light:#24292E}html pre.shiki code .sSdlY, html code.shiki .sSdlY{--shiki-default:#9ABDF5;--shiki-light:#005CC5}html pre.shiki code .sy3HM, html code.shiki .sy3HM{--shiki-default:#9D7CD8;--shiki-light:#D73A49}html pre.shiki code .sBxw3, html code.shiki .sBxw3{--shiki-default:#BB9AF7;--shiki-light:#D73A49}html pre.shiki code .sgjcI, html code.shiki .sgjcI{--shiki-default:#E0AF68;--shiki-light:#E36209}html pre.shiki code .sXJ4V, html code.shiki .sXJ4V{--shiki-default:#A9B1D6;--shiki-light:#6F42C1}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 .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);}html pre.shiki code .sF5CD, html code.shiki .sF5CD{--shiki-default:#A9B1D6;--shiki-light:#005CC5}html pre.shiki code .sGNLY, html code.shiki .sGNLY{--shiki-default:#A9B1D6;--shiki-light:#24292E}",{"title":45,"searchDepth":85,"depth":85,"links":610},[611,612,613,614,615],{"id":21,"depth":67,"text":21},{"id":34,"depth":67,"text":35},{"id":300,"depth":67,"text":301},{"id":481,"depth":67,"text":481},{"id":600,"depth":67,"text":601},"https:\u002F\u002Fpicsum.photos\u002Fseed\u002Fmoonstream-aurora\u002F1600\u002F1000","2026-06-18","animation-timeline 把滚动位置直接变成动画进度，浏览器在合成线程完成一切。本文用阅读进度条和入场动效两个例子，把这套原生 API 讲清楚。",false,"md",{},"\u002Fposts\u002Fcss-scroll-driven-animations",{"title":5,"description":618},"posts\u002Fcss-scroll-driven-animations",[44,626],"动画",null,"XkBB1nFvzBCBg7QE6K5FOCdvosfgYz6kyqghcis5M0M",[630,634],{"title":631,"path":632,"stem":633,"children":-1},"用 Nuxt 4 和 Content 把博客钉在静态文件上","\u002Fposts\u002Fship-a-static-blog-with-nuxt4","posts\u002Fship-a-static-blog-with-nuxt4",{"title":635,"path":636,"stem":637,"children":-1},"Markdown 排版全要素测试","\u002Fposts\u002Fmarkdown-kitchen-sink","posts\u002Fmarkdown-kitchen-sink",1786707084540]