滚动文字显现
随着滚动,文字一个接一个亮起来,让读者按照自己滚动的节奏读完一句话。
向下滚动 ↓
用 GSAP ScrollTrigger 制作随滚动擦洗的文字显现效果。把段落拆成以词为单位的 span(没有空格的语言按字拆分),初始 opacity 为 0.15。注册 ScrollTrigger,把这些 span 补间到 opacity 1,stagger 0.1,ease "none",scrollTrigger 设置为 { trigger: 段落, start: "top 80%", end: "bottom 45%", scrub: true },让每个字按阅读顺序随滚动变亮,往回滚动时再变暗。如果文字位于一个可滚动的面板中而不是页面上,把该元素作为 scroller 传入。使用大号展示字体。把完整句子放在段落的 aria-label 中,并对屏幕阅读器隐藏这些 span。在 prefers-reduced-motion 下所有文字直接完全显示。组件卸载时 revert GSAP context。文字为“动效应该说明发生了什么,引导视线,然后安静退场。”
源代码
'use client'
import { useEffect, useRef } from 'react'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import s from './demo.module.css'
gsap.registerPlugin(ScrollTrigger)
export default function ScrollTextReveal({ labels }: { labels: Record<string, string> }) {
const frame = useRef<HTMLDivElement>(null)
const text = useRef<HTMLParagraphElement>(null)
const spaced = labels.text.includes(' ')
const words = spaced ? labels.text.split(' ') : Array.from(labels.text)
useEffect(() => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
const ctx = gsap.context(() => {
gsap.fromTo(
`.${s.word}`,
{ opacity: 0.15 },
{
opacity: 1,
stagger: 0.1,
ease: 'none',
scrollTrigger: { trigger: text.current, scroller: frame.current, start: 'top 80%', end: 'bottom 45%', scrub: true },
},
)
}, frame)
return () => ctx.revert()
}, [labels.text])
return (
<div ref={frame} className={s.frame} tabIndex={0}>
<span className={s.hint}>{labels.hint} ↓</span>
<p ref={text} className={s.text} aria-label={labels.text}>
{words.map((w, i) => (
<span key={i} className={s.word} aria-hidden="true">
{w}
{spaced && i < words.length - 1 ? ' ' : ''}
</span>
))}
</p>
</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 { padding: 24px 24px 0; }
.text {
max-width: 300px;
margin: 220px auto 200px;
font: 600 var(--text-xl) / 1.35 var(--font-display);
color: var(--stage-ink);
}
.word { display: inline; }