模糊入场文字
标题逐字从模糊变得清晰,每个字落定时微微上升。
用 React 制作一个逐字从模糊变清晰的标题入场效果。用 Array.from 把“逐渐清晰”拆成 inline-block 的 span,并把 --i 设为每个字的序号。每个 span 从 opacity 0、filter blur(12px)、translateY(0.25em) 动画到 opacity 1、blur(0)、translateY(0),时长 700ms,缓动为 cubic-bezier(0.16, 1, 0.3, 1),fill-mode 为 both,延迟为 calc(var(--i) * 45ms)。添加一个“重播”按钮,通过改变外层元素的 key 重新播放动画。可读文字放在 aria-label 中。在 prefers-reduced-motion 下改为整句 150ms 的透明度淡入。
源代码
'use client'
import { useState } from 'react'
import s from './demo.module.css'
export default function BlurInLetters({ labels }: { labels: Record<string, string> }) {
const [run, setRun] = useState(0)
return (
<div className={s.wrap}>
<p key={run} className={s.text} aria-label={labels.text}>
{Array.from(labels.text).map((ch, i) => (
<span key={i} className={s.ch} style={{ '--i': i } as React.CSSProperties} aria-hidden="true">
{ch === ' ' ? ' ' : ch}
</span>
))}
</p>
<button type="button" className={s.button} onClick={() => setRun(run + 1)}>
{labels.replay}
</button>
</div>
)
}
.wrap { display: grid; justify-items: center; gap: 16px; }
.text {
display: flex;
margin: 0;
font: 700 2.6rem / 1.1 var(--font-display);
letter-spacing: -0.03em;
color: var(--stage-ink);
white-space: nowrap;
}
.ch {
display: inline-block;
animation: focus-in 700ms var(--ease-out) both;
animation-delay: calc(var(--i) * 45ms);
}
@keyframes focus-in {
from { opacity: 0; filter: blur(12px); transform: translateY(0.25em); }
}
.button {
height: 32px;
padding: 0 12px;
border: 1px solid var(--stage-rule);
border-radius: 8px;
background: var(--stage-surface);
color: var(--stage-ink);
font: 500 var(--text-sm) / 1 var(--font-body);
white-space: nowrap;
cursor: pointer;
}
.button:hover { border-color: var(--stage-accent); }
.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
.ch { animation: none; }
.text { animation: fade 150ms linear both; }
}
@keyframes fade { from { opacity: 0; } }