06Chữ gợn sóng
Mọi chữ chạy chung một đoạn keyframes translateY từ 0 lên -16px rồi về 0, chỉ khác delay 90ms mỗi chữ qua --i, nên riêng độ lệch delay đã đủ khiến cả dòng gợn sóng liên tục. Vòng lặp không có điểm vào hay điểm ra.
Dùng choGợi ý chờ lúc loading, chữ trang trí cho trạng thái rỗng (empty state)
같은 keyframes지연차 파도translateY

<main class="stage">
<h1 class="wave" data-split>Vui lòng chờ</h1>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 한 글자가 오르내리는 한 주기
$easing: $ease-out;
$stagger: 90ms; // --i 1당 시차 — 이 간격이 파도를 만든다
@include stage($stage-yellow);
.wave {
margin: 0;
display: flex;
gap: clamp(2px, 1vw, 6px);
font-weight: 800; font-size: clamp(28px, 10vw, 46px); line-height: 1; font-family: inherit;
color: $subject-ink;
span {
display: inline-block;
animation: wave-up $duration $easing infinite, letter-blur-in $duration $easing infinite;
animation-delay: calc(var(--i) * #{$stagger} + var(--n) * 70ms); // 노이즈로 파도가 기계적으로 안 맞게
}
}
@keyframes wave-up {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-16px); }
}
@keyframes letter-blur-in {
0%, 100% { filter: blur(0); }
50% { filter: blur(3px); } // 정점에서 살짝 흐려져 움직임 잔상처럼 보인다
}
@media (prefers-reduced-motion: reduce) {
.wave span { animation: none; transform: none; filter: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.wave {
margin: 0;
display: flex;
gap: clamp(2px, 1vw, 6px);
font-weight: 800;
font-size: clamp(28px, 10vw, 46px);
line-height: 1;
font-family: inherit;
color: #17141a;
}
.wave span {
display: inline-block;
animation: wave-up 2s ease-out infinite, letter-blur-in 2s ease-out infinite;
animation-delay: calc(var(--i) * 90ms + var(--n) * 70ms);
}
@keyframes wave-up {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-16px);
}
}
@keyframes letter-blur-in {
0%, 100% {
filter: blur(0);
}
50% {
filter: blur(3px);
}
}
@media (prefers-reduced-motion: reduce) {
.wave span {
animation: none;
transform: none;
filter: none;
}
}(function(){
function splitLetters(el){
var text = el.textContent;
el.textContent = "";
Array.from(text).forEach(function(ch, i){
var span = document.createElement("span");
span.className = "letter";
var seed = Math.sin((i + 1) * 12.9898) * 43758.5453;
var noise = seed - Math.floor(seed);
span.style.setProperty("--i", i);
span.style.setProperty("--n", noise.toFixed(3));
span.textContent = ch === " " ? " " : ch;
el.appendChild(span);
});
}
document.querySelectorAll("[data-split]").forEach(splitLetters);
})();import type { CSSProperties } from "react";
import styles from "./WaveText.module.scss";
type Vars = CSSProperties & { "--i"?: number; "--n"?: number };
function noise(i: number): number {
const s = Math.sin((i + 1) * 12.9898) * 43758.5453;
return Number((s - Math.floor(s)).toFixed(3));
}
export type WaveTextProps = {
letters: string[];
/** 글자들이 한 바퀴 도는 시간 ms (기본 2000) */
duration?: number;
/** --i 1당 시차 ms (기본 90ms) — 이 간격이 파도를 만든다 */
stagger?: number;
color?: string;
};
export default function WaveText({ letters, duration = 2000, color = "#17141a" }: WaveTextProps) {
return (
<h1 className={styles.wave} style={{ "--color": color, "--duration": `${duration}ms` } as CSSProperties}>
{letters.map((ch, i) => (
<span key={i} style={{ "--i": i, "--n": noise(i) } as Vars}>{ch}</span>
))}
</h1>
);
}06 Chữ gợn sóng — Diện tích đo được 6.247% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 CSS Text Animation Effects — dán 1 dòng là chạy
Bài viết chứa demo này
9 CSS Text Animation Effects — dán 1 dòng là chạyCSS text animation effects là các chuyển động chữ dựng thuần bằng CSS, không cần JavaScript, giúp tiêu đề hay câu chữ xu Xem tất cả demo trong bài này

