GODRICH

CSS 타이핑 효과 9종 — 한 줄 복붙으로

CSS 타이핑 효과는 자바스크립트 라이브러리 없이 CSS만으로 문장이 한 글자씩 나타나거나 지워지는 것처럼 보이게 만드는 짧은 애니메이션입니다. 아래 아홉 개는 각각 다른 CSS 속성 하나에 기대고 있고, 필요한 건 격자에서 눈으로 고른 뒤 zip으로 통째로 가져가면 됩니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 인기 순이 아니라 브라우저가 실제로 타이핑을 표현하는 방법 순으로 놓았습니다. 커서만 깜빡이는 것(01)에서 시작해 폭을 단계별로 늘리는 기본기(02), 그 기본기가 다국어에서 깨지는 지점을 고치는 방법(03), 여러 줄(04)과 삭제 후 재입력(05)으로 응용을 넓히고, AI 응답(06)과 터미널(07) 같은 실제 화면, SVG 선 그리기(08)와 단어 교체(09)로 마무리됩니다. 코드는 각 항목의 규칙만 남겼고, 마크업까지 다 붙은 완성 파일은 zip 안에 들어 있습니다.

01커서 깜빡임

타이핑이 끝난 자리에 문자 폭만한 막대를 두고 opacity를 두 값 사이에서 뚝뚝 끊어 전환하면 실제 텍스트 커서처럼 보입니다. 폭이나 위치는 전혀 건드리지 않아 레이아웃 재계산이 없습니다.

step-endopacity 깜빡임::after 커서
.line__caret {
  width: 10px;
  height: 1.3em;
  background: #2f6df6;
  animation: caret-blink 300ms steps(2, jump-none) infinite;
}
@keyframes caret-blink {
  from { opacity: 1; }
  to   { opacity: 0; }
}

02steps() 타자 효과

width를 0에서 글자 수만큼의 ch로 steps()를 이용해 한 글자씩 끊어 늘리면 실제 타자기가 찍는 것과 같은 리듬이 나옵니다. 자간과 글자 폭이 완전히 고정폭이 아니라면 마지막 한두 글자가 살짝 밀려 보일 수 있습니다.

steps(n)ch 단위white-space: nowrap
.type__text {
  overflow: hidden;
  white-space: nowrap;
  width: 0;
  border-right: .12em solid #17141a;
  animation: type-loop 2s linear infinite;
}
@keyframes type-loop {
  0%, 6%  { width: 0; animation-timing-function: steps(10, end); }
  58%     { width: 10ch; }
  88%     { width: 10ch; }
  94%, 100% { width: 0; }
}

03다국어 글자 조합 타이핑

steps()와 ch는 고정폭 글꼴을 전제해서 한글 자모 조합이나 이모지가 섞이면 한 스텝에 반 글자만 드러나는 경우가 생깁니다. 캔버스로 각 글자의 실제 픽셀 폭을 재서 그 경계에서만 clip-path가 멈추게 하면 어떤 언어가 섞여도 글자 중간에서 잘리지 않습니다.

clip-path 스텝getBoundingClientRect가변폭 대응
var w = 0, cum = [0];
for (var i = 0; i < s.length; i++) {
  w += ctx.measureText(s[i]).width;
  cum.push(w);
}
var kf = cum.map(function (px) {
  return { clipPath: 'inset(0 ' + (100 - px / w * 100) + '% 0 0)' };
});
el.animate(kf, { duration: 2000, iterations: Infinity });

04여러 줄 타이핑

줄마다 따로 keyframes를 두고 각자의 정지 구간과 steps() 구간을 퍼센트로 배치하면, 위 줄이 다 찍히는 시점에 맞춰 다음 줄이 이어서 시작하는 것처럼 보입니다. 세 줄 모두 같은 2초 루프를 공유하기 때문에 별도의 지연 값을 손으로 맞출 필요가 없습니다.

줄마다 steps()퍼센트 구간 분리overflow: hidden
@keyframes l2-type {
  0%, 10% { width: 0; animation-timing-function: steps(16, end); }
  34%     { width: 16ch; }
  88%     { width: 16ch; }
  96%, 100% { width: 0; }
}
.lines__text--2 { animation: l2-type 2s ease-out infinite; }

05지우고 다시 타이핑

width가 0에서 끝까지 갔다가 다시 0으로 돌아오는 keyframes 하나에 타이핑과 삭제를 같이 담으면 자바스크립트 없이도 검색창 placeholder 같은 반복 문구를 만들 수 있습니다. 타이핑 구간과 삭제 구간에 각각 다른 지점에서 steps()를 다시 걸어야 두 방향 모두 끊어지는 느낌이 유지됩니다.

단일 keyframes0→100%→0hold 구간
@keyframes search-loop {
  0%, 6%   { width: 0; animation-timing-function: steps(11, end); }
  46%      { width: 11ch; }
  62%      { width: 11ch; animation-timing-function: steps(11, end); }
  96%, 100% { width: 0; }
}

06AI 답변 스트리밍 느낌

고정 간격 steps() 대신 글자마다 30에서 90ms 사이 무작위 값을 뽑아 누적한 뒤 그 비율을 offset으로 넘기면, 재생될 때마다 조금씩 다른 리듬으로 글자가 나타나 실제 토큰 스트리밍처럼 보입니다. 옆의 작은 원은 opacity와 transform만으로 따로 맥동해 응답이 아직 오고 있다는 신호를 더합니다.

랜덤 간격 JSsetTimeout 루프오브 커서 pulse
var acc = 0, kf = [{ clipPath: 'inset(0 100% 0 0)', offset: 0 }];
for (var i = 0; i < n; i++) {
  acc += delays[i];
  kf.push({
    clipPath: 'inset(0 ' + (100 - acc / sum * 78) + '% 0 0)',
    offset: acc / sum * .78
  });
}
el.animate(kf, { duration: 2000, iterations: Infinity });

07터미널 프롬프트

명령어 줄은 steps()로 찍히고, 그 애니메이션이 끝나는 지점 이후 구간에서만 응답 줄의 opacity가 0에서 1로 바뀌도록 같은 keyframes 퍼센트 안에 순서를 담으면 두 단계가 자연스럽게 이어집니다. 두 애니메이션이 같은 2초 루프를 쓰기 때문에 지연 값 없이도 명령어와 응답의 순서가 항상 지켜집니다.

명령어→응답 순서monospace 터미널 폰트트래픽라이트 장식
@keyframes resp-fade {
  0%, 44% { opacity: 0; }
  50%     { opacity: 1; }
  82%     { opacity: 1; }
  90%, 100% { opacity: 0; }
}
.term__resp { animation: resp-fade 2s ease-out infinite; }

08SVG 손글씨 효과

path의 stroke-dasharray를 선 전체 길이보다 넉넉하게 두고 stroke-dashoffset을 그 값에서 0으로 트랜지션하면, 획이 순서대로 그려지는 손글씨 느낌이 납니다. 정확한 길이를 몰라도 되기 때문에 복잡한 곡선을 다시 그릴 때마다 값을 재는 수고가 없습니다.

stroke-dasharraystroke-dashoffsetpath 길이 측정
path {
  stroke-dasharray: 520;
  stroke-dashoffset: 520;
  animation: draw 2s ease-out infinite;
}
@keyframes draw {
  0%, 6%    { stroke-dashoffset: 520; }
  55%       { stroke-dashoffset: 0; }
  80%       { stroke-dashoffset: 0; }
  96%, 100% { stroke-dashoffset: 520; }
}

09단어 교체 타이핑

같은 자리에 단어 세 개를 겹쳐 두고 각자의 keyframes에 자기 차례의 구간만 내주면, 문장의 나머지는 그대로 둔 채 그 구간에서만 폭 0에서 시작해 타이핑되고 다시 지워집니다. 텍스트 노드를 자바스크립트로 바꿔치기하지 않기 때문에 리액트에서도 동일한 keyframes 세 개만 그대로 옮기면 됩니다.

구간별 steps()단어 3개 겹쳐두기고정 문장 + 가변 단어
@keyframes w2 {
  0%, 40%   { width: 0; animation-timing-function: steps(3, end); }
  46%       { width: 3ch; }
  68%       { width: 3ch; animation-timing-function: steps(3, end); }
  74%, 100% { width: 0; }
}
.swap__word--2 { position: absolute; left: 0; animation: w2 2s linear infinite; }

어디서 깨지는가 — 함정 하나

width를 max-content 같은 고유 크기 키워드로 트랜지션하면 언어별로 텍스트 길이가 달라져도 잘리지 않을 거라 기대하기 쉽지만, 실측해 보면 값이 계단식으로 나뉘지 않고 시작 값에서 끝 값으로 한 번에 건너뜁니다. 렌더 측정에서 스물세 프레임 중 단 두 프레임만 바뀐 것으로 잡혔고, 화면으로 봐도 타이핑이 아니라 텍스트가 순간 나타나는 것처럼 보였습니다. width와 max-content 사이의 보간은 interpolate-size 관련 MDN 문서가 다루는 영역이고, 이 값을 켜지 않은 브라우저에서는 이산 애니메이션으로 처리됩니다. 아직 지원이 갈리기 때문에(caniuse의 interpolate-size 지원표) 아홉 개 모두 자바스크립트로 실제 렌더 폭을 캔버스에서 재서 CSS 변수에 넣고, 그 변수를 픽셀 길이로 트랜지션하는 방식으로 고쳤습니다. 이렇게 고친 아홉 개 파일의 압축 비밀번호는 erv8c6eb이고, 아래 버튼으로 받을 수 있습니다.

두 번째로 짚을 것은 03의 clip-path 방식입니다. steps() 대신 캔버스로 잰 실제 글자 폭을 offset으로 직접 넘기기 때문에, 폰트가 아직 로드되지 않은 시점에 측정하면 좁은 폭 기준으로 어긋난 값이 그대로 굳어 버립니다. 폰트 로드가 끝난 뒤에 측정하도록 순서를 맞추는 것이 이 기법의 전제입니다.

접근성

아홉 개 전부 prefers-reduced-motion: reduce에서 트랜지션과 keyframes를 꺼서 완성된 문장이나 획만 즉시 보여주고, 자바스크립트로 만든 03·06의 clip-path 애니메이션도 같은 미디어 쿼리 안에서 el.animate() 호출 자체를 건너뛰도록 처리했습니다. 커서나 원처럼 장식만 담당하는 요소는 애니메이션을 끄는 대신 opacity를 고정값으로 남겨 정보가 사라지지 않게 했습니다. 세부 값 조합은 MDN의 prefers-reduced-motion 문서에 정리돼 있습니다.

@media (prefers-reduced-motion: reduce) {
  .type__text { animation: none; width: var(--full, 10ch); border-right-color: transparent; }
}

CSS 텍스트·문자 관련 다른 주제는 CSS 애니메이션 카테고리 안에도 있고, 이 블로그가 데모를 어떻게 만들고 검증하는지는 소개 페이지에 적어 뒀습니다.

FAQ

CSS만으로 타이핑 효과를 만들 수 있습니까?

네, 01·02·04·05·07·08은 자바스크립트 없이 CSS keyframes만으로 동작합니다. 다만 텍스트 길이가 바뀔 때마다 완성 폭 값도 같이 바꿔야 끝까지 정확히 드러납니다.

03과 06은 왜 자바스크립트가 필요합니까?

03은 언어마다 다른 실제 글자 폭을 측정해야 하고, 06은 무작위 간격을 미리 계산해 el.animate()에 keyframes로 넘겨야 하기 때문입니다. 두 경우 모두 CSS 값만으로는 얻을 수 없는 수치입니다.

타이핑 속도를 늦추거나 빠르게 하려면 어디를 고칩니까?

각 항목의 SCSS 맨 위에 있는 $duration 변수 하나만 바꾸면 됩니다. steps() 개수는 텍스트 길이에 맞춰져 있어 속도를 바꿔도 끊어지는 리듬 자체는 유지됩니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.