09Gõ chữ đổi từ
Ba từ xếp chồng lên cùng một vị trí, giữ width bằng 0 cho đến đúng lượt của mình rồi mới gõ và xoá bằng steps(), nên chỉ một từ đổi trong khi phần còn lại của câu đứng yên.
Dùng choCâu hero đổi vai trò hoặc chức danh, slogan giới thiệu nhiều tính năng
구간별 steps()단어 3개 겹쳐두기고정 문장 + 가변 단어

<main class="stage">
<p class="swap">
<span class="swap__prefix">Đây chính là công cụ dành cho </span><span class="swap__slot"><span class="swap__word swap__word--1">Designer</span><span class="swap__word swap__word--2">Developer</span><span class="swap__word swap__word--3">Marketer</span></span><span class="swap__suffix"> — hãy thử ngay</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 Gõ chữ đổi từ — Diện tích đo được 4.917% · khung hình chuyển động 11/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 kiểu css typing animation, copy dán là chạy
Bài viết chứa demo này
9 kiểu css typing animation, copy dán là chạyCss typing animation là hiệu ứng mô phỏng việc gõ chữ bằng CSS thuần, dựng bằng steps() và một con trỏ nhấp nháy thay ch Xem tất cả demo trong bài này


