09단어 교체 타이핑
같은 자리에 겹쳐 둔 단어 세 개가 폭 0인 채로 대기하다가 자기 차례의 구간에서만 steps()로 타이핑되고 지워져, 문장의 나머지는 그대로 두고 한 단어만 바뀌는 것처럼 보이게 한다.
쓰임역할·직군이 바뀌는 히어로 문구, 다중 기능 소개 슬로건
구간별 steps()단어 3개 겹쳐두기고정 문장 + 가변 단어

<main class="stage">
<p class="swap">
<span class="swap__prefix">이건 바로 </span><span class="swap__slot"><span class="swap__word swap__word--1">디자이너</span><span class="swap__word swap__word--2">개발자</span><span class="swap__word swap__word--3">마케터</span></span><span class="swap__suffix"> 를 위한 도구예요</span>
</p>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 단어 세 개가 차례로 도는 전체 루프
$easing: $ease-out; // 문장 색·굵기 톤
$color: $subject-blue; // 바뀌는 단어 색
@include stage($stage-ink);
.swap {
margin: 0;
font-weight: 700; font-size: clamp(17px, 4.8vw, 27px); line-height: 1.5; font-family: inherit;
color: $subject-cream;
&__slot { position: relative; display: inline-flex; vertical-align: bottom; }
&__word {
display: inline-block;
overflow: hidden;
white-space: nowrap;
width: 0;
border-right: .1em solid $color;
color: $color;
&--1 { animation: w1 $duration linear infinite; }
&--2 { position: absolute; left: 0; top: 0; animation: w2 $duration linear infinite; }
&--3 { position: absolute; left: 0; top: 0; animation: w3 $duration linear infinite; }
}
}
@keyframes w1 {
0% { width: 0; animation-timing-function: steps(4, end); }
8% { width: var(--full, 4ch); }
30% { width: var(--full, 4ch); animation-timing-function: steps(4, end); }
38%, 100% { width: 0; }
}
@keyframes w2 {
0%, 40% { width: 0; animation-timing-function: steps(3, end); }
46% { width: var(--full, 4ch); }
68% { width: var(--full, 4ch); animation-timing-function: steps(3, end); }
74%, 100% { width: 0; }
}
@keyframes w3 {
0%, 74% { width: 0; animation-timing-function: steps(3, end); }
80% { width: var(--full, 4ch); }
96% { width: var(--full, 4ch); animation-timing-function: steps(3, end); }
100% { width: 0; }
}
@media (prefers-reduced-motion: reduce) {
.swap__word { animation: none; }
.swap__word--1 { width: var(--full, 4ch); }
.swap__word--2,
.swap__word--3 { width: 0; opacity: 0; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.swap {
margin: 0;
font-weight: 700;
font-size: clamp(17px, 4.8vw, 27px);
line-height: 1.5;
font-family: inherit;
color: #fff7e6;
}
.swap__slot {
position: relative;
display: inline-flex;
vertical-align: bottom;
}
.swap__word {
display: inline-block;
overflow: hidden;
white-space: nowrap;
width: 0;
border-right: 0.1em solid #2f6df6;
color: #2f6df6;
}
.swap__word--1 {
animation: w1 2s linear infinite;
}
.swap__word--2 {
position: absolute;
left: 0;
top: 0;
animation: w2 2s linear infinite;
}
.swap__word--3 {
position: absolute;
left: 0;
top: 0;
animation: w3 2s linear infinite;
}
@keyframes w1 {
0% {
width: 0;
animation-timing-function: steps(4, end);
}
8% {
width: var(--full, 4ch);
}
30% {
width: var(--full, 4ch);
animation-timing-function: steps(4, end);
}
38%, 100% {
width: 0;
}
}
@keyframes w2 {
0%, 40% {
width: 0;
animation-timing-function: steps(3, end);
}
46% {
width: var(--full, 4ch);
}
68% {
width: var(--full, 4ch);
animation-timing-function: steps(3, end);
}
74%, 100% {
width: 0;
}
}
@keyframes w3 {
0%, 74% {
width: 0;
animation-timing-function: steps(3, end);
}
80% {
width: var(--full, 4ch);
}
96% {
width: var(--full, 4ch);
animation-timing-function: steps(3, end);
}
100% {
width: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.swap__word {
animation: none;
}
.swap__word--1 {
width: var(--full, 4ch);
}
.swap__word--2,
.swap__word--3 {
width: 0;
opacity: 0;
}
}(function () {
var cv = document.createElement('canvas').getContext('2d');
document.querySelectorAll('.swap__word').forEach(function (el) {
cv.font = getComputedStyle(el).font;
el.style.setProperty('--full', Math.ceil(cv.measureText(el.textContent).width) + 'px');
});
})();import { useEffect, useRef } from "react";
import type { CSSProperties } from "react";
import styles from "./WordSwap.module.scss";
export type WordSwapProps = {
prefix: string;
words: [string, string, string];
suffix: string;
/** 단어 세 개가 차례로 도는 전체 루프 (기본 2000ms) */
duration?: number;
color?: string;
};
export default function WordSwap({ prefix, words, suffix, duration = 2000, color = "#2f6df6" }: WordSwapProps) {
const r1 = useRef<HTMLSpanElement | null>(null);
const r2 = useRef<HTMLSpanElement | null>(null);
const r3 = useRef<HTMLSpanElement | null>(null);
useEffect(() => {
const cv = document.createElement("canvas").getContext("2d");
if (!cv) return;
[r1, r2, r3].forEach((r) => {
const el = r.current;
if (!el) return;
cv.font = getComputedStyle(el).font;
el.style.setProperty("--full", Math.ceil(cv.measureText(el.textContent || "").width) + "px");
});
}, [words]);
const vars = { "--duration": `${duration}ms`, "--color": color } as CSSProperties;
const e = (s: string) => ({ "--easing": `steps(${s.length}, end)` } as CSSProperties);
return (
<p className={styles.swap} style={vars}>
<span className={styles.prefix}>{prefix} </span>
<span className={styles.slot}>
<span ref={r1} className={`${styles.word} ${styles.w1}`} style={e(words[0])}>{words[0]}</span>
<span ref={r2} className={`${styles.word} ${styles.w2}`} style={e(words[1])}>{words[1]}</span>
<span ref={r3} className={`${styles.word} ${styles.w3}`} style={e(words[2])}>{words[2]}</span>
</span>
<span className={styles.suffix}> {suffix}</span>
</p>
);
}09 단어 교체 타이핑 — 측정 누적면적 4.917% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 타이핑 효과 9종 — 한 줄 복붙으로
이 데모가 속한 글
CSS 타이핑 효과 9종 — 한 줄 복붙으로CSS 타이핑 효과는 자바스크립트 라이브러리 없이 CSS만으로 문장이 한 글자씩 나타나거나 지워지는 것처럼 보이게 만드는 짧은 애니메이션입니다. 아래 아홉 개는 각각 다른 CSS 속성 하나에 기대고 있고, 필요한 건 이 글의 데모 전부 보기


