滚动控制
ui
01进度条在补什么
滚动条常常被样式藏起来,读者失去了「还剩多少」的参照。进度条把不可见的滚动位置翻译成一条看得懂的线,让人知道自己站在哪儿。
02useScroll 的两种来源
不传参数时它读整个页面的滚动;传入 container 之后,进度只属于这个可滚动元素,外面的页面滚不滚都与它无关。
03scrollYProgress 是什么
一个从 0 到 1 的 MotionValue。0 是还没开始,1 是已经到底。它在动画帧里更新,不会触发 React 重渲染,所以滚得再快也不掉帧。
04为什么要套一层 spring
原始进度和滚轮是一比一的,手一停它就停。用 useSpring 包一层,进度条会带着惯性追上来,顺带把触控板的细碎抖动也抹平了。
05用 scaleX 而不是改宽度
缩放走的是 GPU 合成层,不触发布局回流;宽度不行。再配上 origin-left,条子才是从左边长出来,而不是从中间往两头撑。
06stiffness 与 damping
stiffness 决定追得多急,damping 决定停得多稳。200 / 40 是一组克制的取值:跟手,但停下时不会来回晃。
07它是氛围,不是导航
进度条只回答「还有多远」,回答不了「我要去哪儿」。需要跳转的长文,该给目录还是得给目录。
08最容易踩的坑
container 指向的元素必须自己能滚:既要 overflow-y-auto,也要有确定的高度。少给高度,scrollYProgress 会一直停在 0。
src
TSX
// npm install motion
"use client";
import { useRef } from "react";
import { motion, useScroll, useSpring } from "motion/react";
const chapters = [
{
title: "进度条在补什么",
body: "滚动条常常被样式藏起来,读者失去了「还剩多少」的参照。进度条把不可见的滚动位置翻译成一条看得懂的线,让人知道自己站在哪儿。",
},
{
title: "useScroll 的两种来源",
body: "不传参数时它读整个页面的滚动;传入 container 之后,进度只属于这个可滚动元素,外面的页面滚不滚都与它无关。",
},
{
title: "scrollYProgress 是什么",
body: "一个从 0 到 1 的 MotionValue。0 是还没开始,1 是已经到底。它在动画帧里更新,不会触发 React 重渲染,所以滚得再快也不掉帧。",
},
{
title: "为什么要套一层 spring",
body: "原始进度和滚轮是一比一的,手一停它就停。用 useSpring 包一层,进度条会带着惯性追上来,顺带把触控板的细碎抖动也抹平了。",
},
{
title: "用 scaleX 而不是改宽度",
body: "缩放走的是 GPU 合成层,不触发布局回流;宽度不行。再配上 origin-left,条子才是从左边长出来,而不是从中间往两头撑。",
},
{
title: "stiffness 与 damping",
body: "stiffness 决定追得多急,damping 决定停得多稳。200 / 40 是一组克制的取值:跟手,但停下时不会来回晃。",
},
{
title: "它是氛围,不是导航",
body: "进度条只回答「还有多远」,回答不了「我要去哪儿」。需要跳转的长文,该给目录还是得给目录。",
},
{
title: "最容易踩的坑",
body: "container 指向的元素必须自己能滚:既要 overflow-y-auto,也要有确定的高度。少给高度,scrollYProgress 会一直停在 0。",
},
];
export default function Page() {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({ container: ref });
const scaleX = useSpring(scrollYProgress, { stiffness: 200, damping: 40 });
return (
<div className="mx-auto max-w-4xl space-y-5 px-10 py-20">
<motion.div
style={{ scaleX }}
className="h-2 origin-left bg-gradient-to-r from-purple-700 to-teal-400"
/>
<div ref={ref} className="h-[480px] overflow-y-auto pr-4">
{chapters.map((c, i) => (
<section key={c.title} className="mb-10 space-y-3">
<h3 className="text-2xl font-bold text-white/90">
<span className="mr-2 font-mono text-white">
{String(i + 1).padStart(2, "0")}
</span>
{c.title}
</h3>
<p className="leading-relaxed text-white/60">{c.body}</p>
</section>
))}
</div>
</div>
);
}