阅读进度条
顶部的细条随着你往下读不断填满,让读者随时知道还剩多少。
0% 已读
设计平静的界面
用 React 为文章制作阅读进度条。用被动监听器监听滚动元素(整页时为 window)的 scroll 事件,按 scrollTop / (scrollHeight - clientHeight) 计算进度,并把它写入容器的 CSS 自定义属性 --p,而不是 React state,这样滚动时不会触发重新渲染。顶部的 sticky 进度条使用 transform: scaleX(var(--p)),transform-origin 为 left,背景为主色;旁边用一个小号等宽字体标签显示四舍五入后的百分比。进度条不加过渡,确保精确跟随滚动。在支持 animation-timeline: scroll() 的浏览器中,也可以改用纯 CSS 实现。在 prefers-reduced-motion 下保持不变,因为它只会随用户滚动而移动。文章标题为“设计平静的界面”。
源代码
'use client'
import { useRef } from 'react'
import s from './demo.module.css'
export default function ScrollProgress({ labels }: { labels: Record<string, string> }) {
const pct = useRef<HTMLSpanElement>(null)
const onScroll = (e: React.UIEvent<HTMLDivElement>) => {
const el = e.currentTarget
const p = el.scrollTop / (el.scrollHeight - el.clientHeight || 1)
el.style.setProperty('--p', String(p))
if (pct.current) pct.current.textContent = `${Math.round(p * 100)}% ${labels.read}`
}
return (
<div className={s.frame} tabIndex={0} onScroll={onScroll}>
<div className={s.top}>
<span className={s.bar} />
<span ref={pct} className={s.pct}>
0% {labels.read}
</span>
</div>
<article className={s.article}>
<h3 className={s.title}>{labels.title}</h3>
{Array.from({ length: 14 }, (_, i) => (
<span key={i} className={s.line} style={{ width: `${[100, 94, 98, 70][i % 4]}%` }} />
))}
</article>
</div>
)
}
.frame {
position: absolute;
inset: 0;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
}
.frame:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -4px; }
.hint {
display: block;
font: 500 var(--text-xs) / 1 var(--font-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
text-align: center;
color: var(--stage-muted);
}
.frame { --p: 0; }
.top {
position: sticky;
top: 0;
z-index: 1;
display: flex;
justify-content: flex-end;
padding: 10px 16px 8px;
background: var(--stage-bg);
}
.bar {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: var(--stage-accent);
transform: scaleX(var(--p));
transform-origin: left;
}
.pct {
font: 500 var(--text-xs) / 1 var(--font-mono);
font-variant-numeric: tabular-nums;
color: var(--stage-muted);
}
.article {
display: grid;
gap: 12px;
max-width: 300px;
margin: 0 auto;
padding: 8px 16px 40px;
}
.title {
margin: 0 0 6px;
font: 600 var(--text-xl) / 1.2 var(--font-display);
color: var(--stage-ink);
}
.line {
display: block;
height: 8px;
border-radius: 4px;
background: var(--stage-surface);
}