GODRICH

CSS 텍스트 애니메이션 효과 9종 — 한 줄 복붙으로

CSS 텍스트 애니메이션 효과는 자바스크립트 없이 CSS만으로 제목이나 문구가 움직이며 나타나거나 강조되는 짧은 연출입니다. 아홉 개 전부 transform·opacity·filter 세 가지 속성만 움직이고, 격자에서 눈으로 고른 뒤 마음에 드는 것만 쓰거나 아홉 개를 zip 하나로

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

아홉 개는 인기 순이 아니라 텍스트가 화면에서 거치는 순서로 놓았습니다. 처음 등장하는 방식 다섯 가지(01~05)로 시작해, 등장 뒤에도 계속 도는 대기 상태(06)를 거쳐, 이미 떠 있는 문구에 시선을 붙잡는 강조 두 가지(07~08)로 넘어가고, 마지막은 오류처럼 불안정해 보이는 상태(09)로 마칩니다. 코드는 항목마다 핵심 규칙만 남겼고, 마크업까지 다 붙은 완성 파일은 zip 안에 있습니다.

01밑에서 마스크 등장

제목을 담은 상자를 overflow: hidden으로 잘라 두고, 안쪽 글자만 translateY로 상자 바깥(115%)에서 0으로 밀어 올리면 커튼을 걷듯 한 번에 드러납니다. 상자 자체는 움직이지 않고 안쪽 글자만 이동하기 때문에 주변 레이아웃은 전혀 흔들리지 않습니다.

overflow:hiddentranslateY 마스크0.5초
.mask { overflow: hidden; max-width: min(380px, 90vw); }
.mask__text {
  color: #fff7e6;
  animation: mask-up 2s cubic-bezier(.2, .8, .2, 1) infinite;
}
@keyframes mask-up {
  0%, 4%    { transform: translateY(115%); }
  30%, 78%  { transform: translateY(0); }
  96%, 100% { transform: translateY(115%); }
}

02글자별 튀어오름

로고나 히어로 타이틀의 글자를 하나씩 <span>으로 나누고 인라인 style="--i:N"을 매긴 뒤, animation-delay: calc(var(--i) * 40ms) 한 줄로 시차를 계산합니다. 모든 글자가 같은 keyframes를 공유하기 때문에 글자 수가 바뀌어도 시차 공식은 그대로 씁니다.

--i 변수40ms 간격translateY 바운스
.letters span {
  display: inline-block;
  animation: letter-bounce 2s cubic-bezier(.2, 1.4, .4, 1) infinite;
  animation-delay: calc(var(--i) * 40ms);
}
@keyframes letter-bounce {
  0%, 4%    { opacity: 0; transform: translateY(22px); }
  22%, 70%  { opacity: 1; transform: translateY(0); }
  88%, 100% { opacity: 0; transform: translateY(-14px); }
}

03회전 등장

글자마다 --i 변수로 90ms씩 지연을 주고 rotate를 -25도에서 0도로, opacity를 0에서 1로 함께 트랜지션하면 살짝 기울어진 채 날아들다 똑바로 서는 것처럼 보입니다. 배지나 태그처럼 짧은 라벨에 어울립니다.

transform: rotateopacity-25deg→0
.rot span {
  animation: rotate-in 300ms ease-out infinite;
  animation-delay: calc(var(--i) * 90ms);
}
@keyframes rotate-in {
  0%, 4%    { opacity: 0; transform: rotate(-25deg); }
  26%, 72%  { opacity: 1; transform: rotate(0deg); }
  90%, 100% { opacity: 0; transform: rotate(18deg); }
}

043D 뒤집기 글자

글자를 담은 부모에 perspective를 주고 각 글자에 backface-visibility: hidden을 둔 채 rotateX를 90도에서 0도로 돌리면, 낱장 카드가 앞으로 넘어오듯 뒤집히며 나타납니다. 가격이나 숫자가 갱신되는 자리에 쓰면 값이 바뀌었다는 신호가 분명해집니다.

perspectiverotateX 90→0backface-visibility
.flip { perspective: 480px; }
.flip span {
  backface-visibility: hidden;
  animation: flip-in 600ms cubic-bezier(.2, .8, .2, 1) infinite;
  animation-delay: calc(var(--i) * 70ms);
}
@keyframes flip-in {
  0%, 4%    { opacity: 0; transform: rotateX(90deg); }
  30%, 74%  { opacity: 1; transform: rotateX(0deg); }
  94%, 100% { opacity: 0; transform: rotateX(-90deg); }
}

05블러+포커스

filter: blur 값을 14px에서 0으로, opacity를 0에서 1로 같이 트랜지션하면 초점이 맞아가듯 문구가 또렷해집니다. transform을 전혀 쓰지 않아 위치는 고정된 채 선명도만 바뀌는 것이 특징입니다.

filter: bluropacity12px→0
.blur {
  animation: blur-focus 2s ease-out infinite;
}
@keyframes blur-focus {
  0%, 4%    { opacity: 0; filter: blur(14px); }
  30%, 76%  { opacity: 1; filter: blur(0); }
  96%, 100% { opacity: 0; filter: blur(14px); }
}

06물결치는 텍스트

글자마다 --i 변수로 90ms씩 지연을 주고 모든 글자가 translateY를 0에서 -16px로 오가는 같은 keyframes를 공유하면, 지연 차이만으로 한 줄 전체가 물결처럼 계속 일렁입니다. 등장이나 퇴장 없이 상시 도는 루프라 로딩 대기 문구에 어울립니다.

같은 keyframes지연차 파도translateY
.wave span {
  animation: wave-up 2s ease-out infinite;
  animation-delay: calc(var(--i) * 90ms);
}
@keyframes wave-up {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-16px); }
}

07흔들리는 강조

문구 전체를 rotate 1~2도와 translateX 2px 사이에서 아주 짧게 왕복시키면 에러 흔들림보다 훨씬 약하게 시선만 붙잡습니다. 루프의 0~60%는 완전히 정지해 있다가 66~90% 구간에서만 흔들리므로, 늘 움직이는 배경 속에서도 그 순간만 튀어 보입니다.

rotate 1~2degtranslateX 2px짧은 왕복
.wiggle {
  animation: wiggle 2s ease-out infinite;
}
@keyframes wiggle {
  0%, 60%, 100% { transform: rotate(0) translateX(0); }
  66% { transform: rotate(-2deg) translateX(-2px); }
  72% { transform: rotate(2deg) translateX(2px); }
  78% { transform: rotate(-2deg) translateX(-2px); }
  84% { transform: rotate(2deg) translateX(2px); }
  90% { transform: rotate(0) translateX(0); }
}

08두 색 스윕

같은 문구를 두 겹으로 쌓아 아래 겹은 고정 색을, 위 겹은 다른 색을 칠한 뒤 위 겹에만 clip-path를 왼쪽에서 오른쪽으로 걷어내는 keyframes를 걸면 그라데이션이 아니라 뚜렷한 두 색 경계로 색이 칠해지는 것처럼 보입니다. 가격표나 캠페인 카피처럼 한순간 시선을 끌어야 하는 자리에 씁니다.

clip-path 스윕두 겹 텍스트단색 경계
.sweep__top {
  position: absolute;
  inset: 0;
  animation: sweep-wipe 2s cubic-bezier(.2, .8, .2, 1) infinite;
}
@keyframes sweep-wipe {
  0%, 4%    { clip-path: inset(0 100% 0 0); }
  40%, 62%  { clip-path: inset(0 0 0 0); }
  96%, 100% { clip-path: inset(0 100% 0 0); }
}

09글리치

같은 문구를 색만 다르게 겹친 두 겹에 mix-blend-mode: screen과 clip-path를 걸어 짧게 translateX로 어긋나게 했다가 되돌리면, 화면이 순간 깨진 듯한 글리치 효과가 납니다. 어긋남을 0~24%, 50~66% 두 구간에 나눠 넣어야 한 번의 짧은 튐이 아니라 두 번 반복되는 리듬으로 보입니다.

clip-path 슬라이스translateX 어긋남색 분리
.glitch__r { mix-blend-mode: screen; animation: glitch-r 2s linear infinite; }
@keyframes glitch-r {
  0%   { transform: translateX(0);    clip-path: inset(0 0 0 0); }
  8%   { transform: translateX(-6px); clip-path: inset(12% 0 58% 0); }
  16%  { transform: translateX(4px);  clip-path: inset(48% 0 14% 0); }
  24%  { transform: translateX(0);    clip-path: inset(0 0 0 0); }
  50%  { transform: translateX(-4px); clip-path: inset(20% 0 40% 0); }
  58%  { transform: translateX(5px);  clip-path: inset(55% 0 8% 0); }
  66%  { transform: translateX(0);    clip-path: inset(0 0 0 0); }
}

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

--i 변수와 calc()로 시차를 계산하는 네 개(02·03·04·06)는 계산이 부모가 아니라 글자 자신의 animation-delay에 걸려 있습니다. 그래서 글자 수가 늘어나도 SCSS를 손댈 필요가 없다는 장점이 있는 대신, 부모 요소에 opacity 트랜지션을 따로 걸어 두면 그 트랜지션은 --i를 모르기 때문에 글자마다 다른 시점에 시작하지 않고 전부 동시에 움직입니다. 시차는 언제나 글자 자신의 animation에만 있어야 합니다.

두 번째는 밝기입니다. 09 글리치는 mix-blend-mode: screen으로 빨강·파랑 두 겹을 겹쳐 색이 갈라지는 것처럼 보이게 하는데, MDN의 mix-blend-mode 문서에 정리돼 있듯 screen은 배경이 밝을수록 두 색을 하얗게 섞어 지웁니다. 그래서 09는 어두운 무대(ink)에만 뒀고, 밝은 무대에 옮겨 쓰려면 screen 대신 difference나 불투명도가 있는 단색 겹으로 바꿔야 합니다. 실제로 렌더링해 측정해 보니 06 물결은 24프레임 중 23프레임에서 움직임이 잡혔지만 07 흔들리는 강조는 7프레임에서만 잡혔는데, 이 차이까지 반영해 다듬은 아홉 개 파일의 압축 비밀번호는 97me82wy이고 아래 버튼으로 받을 수 있습니다.

흔들리는 강조처럼 좁은 구간에만 움직임을 몰아넣는 효과는 스크롤을 빠르게 내리는 사람에게는 아예 안 보일 수 있습니다. 화면에 들어오자마자 한 번만 재생하고 끝내기보다는, 지금처럼 2초 주기로 반복하거나 등장 직후 짧은 시간 안에 몰아서 재생해야 놓치지 않습니다.

접근성

아홉 개 전부 prefers-reduced-motion: reduce에서 keyframes 애니메이션을 끄고 최종 상태만 즉시 보여줍니다. 05 블러는 opacity 1·filter 없음으로 바로 보이고, 01 마스크와 06 물결처럼 transform만 움직이던 항목은 transform 없음으로 제자리에 멈추며, 02·03·04처럼 글자별로 나눈 span은 opacity 1·transform 없음으로 한 번에 자리를 잡습니다. 07 흔들리는 강조와 09 글리치처럼 정보 전달이 아니라 순수하게 시선을 끄는 장식만 담당하는 움직임은 reduce에서 완전히 꺼도 되지만, 08 두 색 스윕처럼 색 자체가 강조 정보인 경우는 스윕 없이도 최종 색이 바로 보이도록 clip-path를 처음부터 0으로 고정했습니다. 세부 처리 범위는 MDN의 prefers-reduced-motion 문서에서 확인할 수 있습니다.

@media (prefers-reduced-motion: reduce) {
  .mask__text, .blur, .wiggle { animation: none; transform: none; filter: none; opacity: 1; }
  .letters span, .rot span, .flip span, .wave span { animation: none; opacity: 1; transform: none; }
  .sweep__top { animation: none; clip-path: inset(0 0 0 0); }
  .glitch__r, .glitch__b { animation: none; opacity: 0; }
}

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

FAQ

CSS만으로 텍스트 애니메이션을 만들 수 있습니까?

네, 아홉 개 전부 자바스크립트 없이 CSS keyframes만으로 동작합니다. 글자별로 나눈 항목(02·03·04·06)도 <span>을 손으로 나누고 --i 값만 넣으면 되고, 별도의 스크립트로 글자를 쪼갤 필요가 없습니다.

글자 수가 달라지면 코드를 얼마나 고쳐야 합니까?

span 개수만 늘리거나 줄이고 --i 값을 순서대로 매기면 됩니다. animation-delay: calc(var(--i) * 시차) 한 줄이 글자 수와 상관없이 시차를 계산하기 때문에 keyframes나 SCSS 쪽은 그대로 둬도 됩니다.

애니메이션 속도를 조절하려면 어디를 고칩니까?

각 항목 SCSS의 animation 줄에 적힌 재생 시간 값을 바꾸면 됩니다. 02·03·04·06처럼 --i를 쓰는 항목은 $stagger 값도 함께 있어서, 전체 속도와 글자 사이 시차를 따로 조절할 수 있습니다.

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

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