GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

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

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

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

아홉 개는 인기 순이 아니라 텍스트가 화면에서 거치는 순서로 놓았습니다. 처음 등장하는 방식 다섯 가지(01~05), 이를테면 css 텍스트 슬라이드 애니메이션으로 시작해, 등장 뒤에도 계속 도는 대기 상태(06)를 거쳐, 이미 떠 있는 문구에 시선을 붙잡는 강조 두 가지(07~08)로 넘어가고, 마지막은 오류처럼 불안정해 보이는 상태(09)로 마칩니다. 코드는 항목마다 핵심 규칙만 남겼고, 마크업까지 다 붙은 완성 파일은 zip 안에 있습니다. 문장이 통째로 찍히는 쪽이 필요하면 CSS 타이핑 효과 9종이고, 글자 대신 배경이 움직이는 편은 CSS 배경 애니메이션 9종입니다.

01밑에서 마스크 등장

어떻게 움직이나

제목을 담은 상자를 overflow: hidden으로 잘라 두고, 안쪽 글자만 translateY로 상자 바깥(115%)에서 0으로 밀어 올리면 커튼을 걷듯 한 번에 드러납니다. 여기에 글자마다 --n 노이즈만큼 다르게 블러가 걸렸다 풀리는 감쇠를 겹쳐 두어, 마스크가 걷히는 순간과 초점이 맞는 순간이 글자마다 미묘하게 어긋나며 함께 지나갑니다.

어디에 쓰나

상자 자체는 움직이지 않고 안쪽 글자만 이동하기 때문에 주변 레이아웃은 전혀 흔들리지 않습니다.

밑에서 마스크 등장
새 창에서 열기

01 · 밑에서 마스크 등장 — 측정 누적면적 5.417% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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) 한 줄로 시차를 계산합니다. 이 시차에 --n 노이즈를 더해 튀어오르는 동안 글자마다 블러가 풀리는 감쇠가 겹쳐지도록 했더니, 글자들이 기계적으로 딱 맞춰 튀지 않고 살짝 흐트러져 보입니다.

어디에 쓰나

모든 글자가 같은 keyframes를 공유하기 때문에 글자 수가 바뀌어도 시차 공식은 그대로 씁니다.

글자별 튀어오름
새 창에서 열기

02 · 글자별 튀어오름 — 측정 누적면적 8.615% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

--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로 함께 트랜지션하면 살짝 기울어진 채 날아들다 똑바로 서는 것처럼 보입니다. 회전하며 등장하는 동안에도 --n 노이즈로 지연을 조금씩 흩트리고 글자마다 블러가 풀리는 감쇠를 함께 겹쳤습니다.

어디에 쓰나

배지나 태그처럼 짧은 라벨에 어울립니다.

회전 등장
새 창에서 열기

03 · 회전 등장 — 측정 누적면적 4.82% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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도로 돌리면, 낱장 카드가 앞으로 넘어오듯 뒤집히며 나타납니다. 뒤집히는 동안 --n 노이즈로 지연을 살짝 흩트리고 글자마다 블러가 풀리는 감쇠를 함께 걸었습니다.

어디에 쓰나

가격이나 숫자가 갱신되는 자리에 쓰면 값이 바뀌었다는 신호가 분명해집니다.

3D 뒤집기 글자
새 창에서 열기

04 · 3D 뒤집기 글자 — 측정 누적면적 3.824% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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로 같이 트랜지션하면 초점이 맞아가듯 문구가 또렷해집니다. 이제 문구 전체가 한 덩어리로 흐려졌다 풀리는 게 아니라, 글자마다 --n 노이즈로 흩어진 시점에 각자 초점이 맞아가도록 바뀌었습니다.

어디에 쓰나

transform을 전혀 쓰지 않아 위치는 고정된 채 선명도만 바뀌는 것이 특징입니다.

블러+포커스
새 창에서 열기

05 · 블러+포커스 — 측정 누적면적 6.333% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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를 공유하면, 지연 차이만으로 한 줄 전체가 물결처럼 계속 일렁입니다. 파도가 정점에 오른 순간에는 --n 노이즈로 살짝 블러가 걸려 모션 잔상처럼 보이는 겹을 추가했습니다.

어디에 쓰나

등장이나 퇴장 없이 상시 도는 루프라 로딩 대기 문구에 어울립니다.

물결치는 텍스트
새 창에서 열기

06 · 물결치는 텍스트 — 측정 누적면적 6.247% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

같은 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흔들리는 강조

css 텍스트 강조 애니메이션은 언제 쓰나

문구 전체를 rotate 1~2도와 translateX 2px 사이에서 아주 짧게 왕복시키면 에러 흔들림보다 훨씬 약하게 시선만 붙잡습니다. 기존 흔들림(wiggle)은 그대로 두고, 등장할 때 글자마다 블러가 풀리는 감쇠가 먼저 한 번 지나간 뒤에 흔들림이 이어지도록 겹을 하나 더 얹었습니다.

어디에 쓰나

루프의 0~60%는 완전히 정지해 있다가 66~90% 구간에서만 흔들리므로, 늘 움직이는 배경 속에서도 그 순간만 튀어 보입니다.

흔들리는 강조
새 창에서 열기

07 · 흔들리는 강조 — 측정 누적면적 4.14% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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 와이프)은 그대로 두고, 글자마다 --n 노이즈로 흩어진 진입 블러 감쇠를 추가로 얹었습니다.

어디에 쓰나

가격표나 캠페인 카피처럼 한순간 시선을 끌어야 하는 자리에 씁니다.

두 색 스윕
새 창에서 열기

08 · 두 색 스윕 — 측정 누적면적 6.474% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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로 어긋나게 했다가 되돌리면, 화면이 순간 깨진 듯한 글리치 효과가 납니다. 기존 RGB 분리 글리치(glitch-r·glitch-b)는 그대로 두고, 글자마다 --n 노이즈로 흩어진 짧은 진입 블러 감쇠를 추가했습니다.

어디에 쓰나

어긋남을 0~24%, 50~66% 두 구간에 나눠 넣어야 한 번의 짧은 튐이 아니라 두 번 반복되는 리듬으로 보입니다.

글리치
새 창에서 열기

09 · 글리치 — 측정 누적면적 6.18% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

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

밝은 무대에서 글리치가 사라진다

두 번째는 밝기입니다. 09 글리치는 mix-blend-mode: screen으로 빨강·파랑 두 겹을 겹쳐 색이 갈라지는 것처럼 보이게 하는데, MDN의 mix-blend-mode 문서에 정리돼 있듯 screen은 배경이 밝을수록 두 색을 하얗게 섞어 지웁니다. 그래서 09는 어두운 무대(ink)에만 뒀고, 밝은 무대에 옮겨 쓰려면 screen 대신 difference나 불투명도가 있는 단색 겹으로 바꿔야 합니다.

프레임으로 잰 차이

실제로 렌더링해 측정해 보니 06 물결은 24프레임 중 23프레임에서 움직임이 잡혔지만 07 흔들리는 강조는 15프레임에서 잡혔는데, 원래는 7프레임에 그쳤던 것이 글자별 블러 감쇠 겹이 추가되면서 흔들림 구간 바깥에서도 픽셀이 바뀌는 프레임이 늘어난 결과입니다. 이 차이까지 반영해 다듬은 아홉 개 파일의 압축 비밀번호는 97me82wy이고 아래 버튼으로 받을 수 있습니다. 글자를 겹겹이 붙이는 미학까지 가면 맥시멀리즘 콜라주 랜딩 키트가 그 넘침을 섹션 단위로 만들어 둔 편입니다.

스크롤이 빠르면 못 본다

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

접근성

동작 줄이기에서

아홉 개 전부 이제 data-split로 글자를 나누고 등장 시 블러→포커스 감쇠 겹을 얹었는데, prefers-reduced-motion: reduce에서는 이 겹까지 포함해 keyframes 애니메이션을 모두 끄고 최종 상태만 즉시 보여줍니다. 05 블러는 opacity 1·filter 없음으로 바로 보이고, 01 마스크와 06 물결처럼 transform만 움직이던 항목은 transform 없음으로 제자리에 멈추며, 02·03·04처럼 처음부터 숨어 있던 글자는 opacity 1·transform 없음·filter 없음으로 한 번에 자리를 잡습니다.

장식과 정보의 차이

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만으로 동작합니다. 다만 글자를 나누는 작업은 이제 data-split 속성을 붙이면 페이지가 열릴 때 짧은 런타임 스크립트가 텍스트 길이에 맞춰 자동으로 <span>과 --i·--n 값을 매겨 주고, 애니메이션이 시작하는 순간이 되면 스크립트 역할은 끝나고 그 뒤는 CSS keyframes가 전부 맡습니다. 움직임의 대상이 글자가 아니라 그림이라면 CSS 이미지 호버 효과 9종이 같은 자리를 사진 쪽에서 맡습니다.

css 애니메이션 시간차는 글자 수가 달라지면 얼마나 고쳐야 합니까?

손으로 span을 고칠 필요가 없습니다. data-split이 붙은 요소는 글자 수가 몇 개든 스크립트가 텍스트 길이에 맞춰 자동으로 span·--i·--n을 만들어 주기 때문에, 번역마다 글자 수가 달라지는 영어·일본어·베트남어 사본도 그대로 둡니다. animation-delay: calc(var(--i) * 시차 + var(--n) * 지터) 한 줄이 글자 수와 상관없이 시차를 계산하기 때문에 keyframes나 SCSS 쪽도 손댈 필요가 없습니다.

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

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

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

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