Wave Letters
Letters hop one after another so a soft wave keeps rolling through the word.
Make a CSS wave text animation in React. Split the text "Hello there" into one inline-block <span> per character with Array.from, and set a --i custom property on each span to its index. Animate every span with the same keyframe: translateY 0 at 0%, -0.3em at 30%, back to 0 at 60% and hold until 100%, over 1.6s with cubic-bezier(0.65, 0, 0.35, 1), infinite. Offset each letter with animation-delay: calc(var(--i) * 70ms). Replace spaces with a non-breaking space so they keep their width. Put the original text in aria-label on the wrapper and aria-hidden on the spans. Turn the animation off under prefers-reduced-motion.
Source
'use client'
import s from './demo.module.css'
export default function WaveLetters({ labels }: { labels: Record<string, string> }) {
return (
<p 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>
)
}
.text {
display: flex;
margin: 0;
font: 700 2.4rem / 1 var(--font-display);
letter-spacing: -0.02em;
color: var(--stage-ink);
white-space: nowrap;
}
.ch {
display: inline-block;
animation: wave 1.6s var(--ease-in-out) infinite;
animation-delay: calc(var(--i) * 70ms);
}
.ch:nth-child(3n) { color: var(--stage-accent); }
@keyframes wave {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-0.3em); }
}
@media (prefers-reduced-motion: reduce) {
.ch { animation: none; }
}