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

CSS 텍스트 애니메이션 9종 — 등장·강조 모음

CSS 텍스트 애니메이션은 제목이나 문장이 화면에 나타나거나 강조될 때 함께 움직이는 효과로, 코드를 몰라도 파일을 그대로 붙이면 바로 쓸 수 있습니다.

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

아홉 개는 인기 순이 아니라 화면에 문장이 나타나는 과정을 따라갑니다. 먼저 단어·줄 단위로 부드럽게 떠오르거나 밀려 들어오는 등장 효과(01~04)를 지나, 기계식 전광판처럼 튀는 연출과 암호가 풀리듯 글자가 맞춰지는 장난스러운 등장(05~06)을 거쳐, 마지막은 자간이 좁혀지거나 테두리가 채워지거나 그림자가 쌓이며 제목 자체를 강조하는 연출(07~09)로 끝납니다. 글자가 아니라 판 전체가 돌아가며 내용을 바꾸는 쪽은 카드 뒤집기 애니메이션이 담당하고, 커서에 반응하는 카드 표면은 CSS 카드 호버 효과에 모여 있습니다.

01단어 단위 떠오르기

텍스트 등장 애니메이션의 기본

문장이 단어 단위로 끊어져 아래에서 위로 하나씩 떠오르며 나타납니다. 글자 하나하나가 아니라 단어 덩어리로 움직이기 때문에 눈으로 따라가기 편합니다.

단어 단위 떠오르기
새 창에서 열기

01 · 단어 단위 떠오르기 — 측정 누적면적 5.099% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

display:inline-blocktranslateYanimation-delay

시작 시각만 다르게

animation-delay로 단어마다 시작 시각을 다르게 두고, 등장 구간은 관성 lerp 곡선($ease-lerp-settle)으로 목표를 살짝 지나쳤다 정착하게 했으며, 단어마다 blur 깊이(6→9→12px)도 달리해 무거운 단어일수록 늦게 초점이 맞습니다.

.ws-word {
  display: inline-block;
  opacity: 0;
  transform: translateY(16px);
  filter: blur(var(--blur, 6px));
}
@keyframes ws-rise {
  0% { opacity: 0; transform: translateY(16px); filter: blur(var(--blur, 6px)); animation-timing-function: $ease-lerp-settle; }
  20%, 80% { opacity: 1; transform: none; filter: blur(0); }
  100% { opacity: 0; transform: translateY(16px); filter: blur(var(--blur, 6px)); }
}
.ws-line.is-demo .ws-word { animation: ws-rise 2s ease-out infinite; }
.ws-line.is-demo .ws-word:nth-child(2) { animation-delay: 160ms; --blur: 9px; }
.ws-line.is-demo .ws-word:nth-child(3) { animation-delay: 320ms; --blur: 12px; }

02대각선 와이프 마스크

사선으로 열리는 마스크

비스듬한 경계선이 화면을 가로지르며 지나가면 그 뒤로 글자가 드러납니다. 커튼처럼 걷히는 게 아니라 칼로 베어내듯 사선으로 열리는 게 특징입니다.

대각선 와이프 마스크
새 창에서 열기

02 · 대각선 와이프 마스크 — 측정 누적면적 2.394% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

skewX(-18deg)transition0.7s

마스크 색을 맞추면

마스크 색을 배경색과 똑같이 맞춰 두면 transform만 움직여도 이런 와이프 효과가 나오며, 마스크는 관성 lerp 곡선으로 목표 지점을 지나쳤다 정착합니다.

.dw-mask {
  position: absolute;
  inset: -20% -10%;
  background: $stage-yellow;
  transform: skewX(-18deg) translateX(0);
}
@keyframes dw-sweep {
  0%, 10% { transform: skewX(-18deg) translateX(0); }
  45%, 70% { transform: skewX(-18deg) translateX(140%); }
  100% { transform: skewX(-18deg) translateX(0); }
}
.dw-wrap.is-demo .dw-mask { animation: dw-sweep 2s $ease-lerp-settle infinite; }

03확대 팝인

살짝 커졌다 제자리로

글자가 작게 눌려 있다가 원래 크기보다 살짝 커졌다가 제자리로 돌아오며 튕기듯 나타납니다.

확대 팝인
새 창에서 열기

03 · 확대 팝인 — 측정 누적면적 6.413% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scalecubic-bezierovershoot

통통 튀는 구간

scale 값이 1을 살짝 넘겼다가 줄어드는 구간이 통통 튀는 느낌을 만듭니다. 등장 구간은 관성 lerp 곡선으로 목표를 살짝 지나쳤다 정착해 커지는 속도에 무게감이 실립니다.

.sp-text {
  opacity: 0;
  transform: scale(.4);
}
@keyframes sp-pop {
  0% { opacity: 0; transform: scale(.4); animation-timing-function: $ease-lerp-settle; }
  18% { opacity: 1; transform: scale(1.1); }
  30% { transform: scale(.96); }
  40%, 78% { transform: scale(1); opacity: 1; }
  100% { opacity: 0; transform: scale(.4); }
}
.sp-text.is-demo { animation: sp-pop 2s cubic-bezier(.2, 1.4, .4, 1) infinite; }

04줄마다 엇갈려 슬라이드

줄마다 엇갈리는 css 텍스트 슬라이드 애니메이션

여러 줄짜리 문단에서 홀수 줄은 왼쪽, 짝수 줄은 오른쪽에서 밀려 들어와 지그재그로 자리를 잡습니다.

줄마다 엇갈려 슬라이드
새 창에서 열기

04 · 줄마다 엇갈려 슬라이드 — 측정 누적면적 7.392% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateXanimation-delay: 160msalternating delay

반대 방향 translateX 두 벌

줄마다 시작 방향만 반대로 둔 translateX 두 벌이 전부입니다. 각 줄은 관성 lerp 곡선으로 목표 위치를 살짝 지나쳤다 정착하며, 같은 구간에서 blur(7px)→0 으로 초점이 맞습니다.

.ls-line--l { transform: translateX(-40px); }
.ls-line--r { transform: translateX(40px); }
@keyframes ls-in-r {
  0% { opacity: 0; transform: translateX(40px); filter: blur(7px); }
  25%, 80% { opacity: 1; transform: none; filter: blur(0); }
  100% { opacity: 0; transform: translateX(40px); filter: blur(7px); }
}
.ls-block.is-demo .ls-line--r {
  animation: ls-in-r 2s $ease-lerp-settle infinite;
  animation-delay: 160ms;
}

05스플릿 플랩 보드

전광판처럼 넘어간다

공항 전광판처럼 글자판이 위에서 아래로 빠르게 넘어가다가 정답 글자에서 멈춥니다.

스플릿 플랩 보드
새 창에서 열기

05 · 스플릿 플랩 보드 — 측정 누적면적 5.078% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

rotateXsteps()perspective

steps() 가 만드는 기계 느낌

rotateX를 매끈한 곡선이 아니라 steps() 타이밍으로 움직여 뚝뚝 끊기는 기계식 느낌을 냈습니다. 판이 모서리로 서는 지점에서 살짝 흐려졌다 착지하며 초점이 맞는, 모션 블러 한 겹을 얹었습니다.

.sf-char {
  transform-origin: center;
  transform: rotateX(90deg);
  opacity: 0;
  filter: blur(3px);
}
@keyframes sf-flip {
  0% { transform: rotateX(90deg); opacity: 0; filter: blur(3px); }
  15% { filter: blur(4px); }
  30% { transform: rotateX(0deg); opacity: 1; filter: blur(0); }
  90% { transform: rotateX(0deg); opacity: 1; filter: blur(0); }
  95% { filter: blur(4px); }
  100% { transform: rotateX(90deg); opacity: 0; filter: blur(3px); }
}
.sf-board.is-demo .sf-char { animation: sf-flip 2s steps(3, end) infinite; }
.sf-board.is-demo .sf-char:nth-child(2) { animation-delay: 120ms; }

06스크램블 디코드

암호가 풀리듯 멈춘다

무작위 글자가 빠르게 바뀌다가 왼쪽부터 하나씩 진짜 글자로 멈춰, 암호가 풀리는 것처럼 보입니다.

스크램블 디코드
새 창에서 열기

06 · 스크램블 디코드 — 측정 누적면적 2.665% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

setIntervalJS 12줄monospace

CSS만으로는 안 되는 이유

CSS만으로는 글자 내용 자체를 바꿀 수 없어서 짧은 자바스크립트로 처리합니다. 몇 글자가 잠겼는지가 곧 진행률이라 별도의 이징 계산이 필요 없습니다.

var frame = 0;
setInterval(function () {
  frame++;
  var locked = Math.max(0, Math.floor((frame % 26) / 3) - 2);
  var out = "";
  for (var i = 0; i < target.length; i++) {
    out += i < locked ? target[i] : glyphs[Math.floor(Math.random() * glyphs.length)];
  }
  el.textContent = out;
}, 60);

07자간 좁혀 등장

넓게 벌어졌다 모인다

글자 사이 간격이 넓게 벌어진 채 투명하게 숨어 있다가 점점 좁아지며 또렷한 제목으로 모입니다.

자간 좁혀 등장
새 창에서 열기

07 · 자간 좁혀 등장 — 측정 누적면적 3.327% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

custom propertytranslateXopacity

letter-spacing 대신 transform

letter-spacing을 직접 움직이는 대신 글자마다 --x라는 커스텀 속성으로 시작 위치를 주고 translateX로 모이게 했습니다. 수렴 구간은 관성 lerp 곡선으로 목표를 살짝 지나쳤다 정착하며, 좁아질수록 blur가 걷혀 또렷해집니다.

.tk-ch {
  display: inline-block;
  opacity: 0;
  transform: translateX(var(--x, 0));
  filter: blur(6px);
}
@keyframes tk-converge {
  0% { opacity: 0; transform: translateX(var(--x, 0)); filter: blur(6px); }
  35%, 80% { opacity: 1; transform: translateX(0); filter: blur(0); }
  100% { opacity: 0; transform: translateX(var(--x, 0)); filter: blur(6px); }
}
.tk-word.is-demo .tk-ch { animation: tk-converge 2s $ease-lerp-settle infinite; }

08테두리에서 채우기로

테두리에서 채워진다

글자가 속이 빈 테두리 상태로 먼저 보이다가, 왼쪽부터 안쪽 색이 차오르며 꽉 찬 글자로 바뀝니다.

테두리에서 채우기로
새 창에서 열기

08 · 테두리에서 채우기로 — 측정 누적면적 2.835% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

-webkit-text-strokeclip-pathoverflow:hidden

두 겹을 겹치는 방법

-webkit-text-stroke로 테두리만 있는 글자를 밑에 깔고, 그 위에 clip-path로 잘라낸 채색 글자를 겹쳐 뒀습니다. 채움이 흘러드는 동안 blur(6px)→0 으로 잉크가 스며들 듯 초점이 맞습니다.

.fl-text--outline {
  color: transparent;
  -webkit-text-stroke: 1px $color;
}
.fl-text--fill {
  position: absolute;
  inset: 0;
  color: $color;
  clip-path: inset(0 100% 0 0);
  filter: blur(6px);
}
@keyframes fl-wipe {
  0%, 10% { clip-path: inset(0 100% 0 0); filter: blur(6px); }
  55%, 70% { clip-path: inset(0 0 0 0); filter: blur(0); }
  90% { clip-path: inset(0 0 0 0); filter: blur(0); }
  100% { clip-path: inset(0 100% 0 0); filter: blur(6px); }
}
.fl-wrap.is-demo .fl-text--fill { animation: fl-wipe 2s ease-out infinite; }

09그림자 쌓아 올리기

css 텍스트 강조 애니메이션으로 쌓는 그림자

평평했던 글자 뒤로 그림자 층이 겹겹이 쌓이며 입체적인 두께가 생겼다가 다시 평평해집니다.

그림자 쌓아 올리기
새 창에서 열기

09 · 그림자 쌓아 올리기 — 측정 누적면적 4.672% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

text-shadowmultiple layerstransition

겹을 하나씩 더한다

text-shadow에 겹을 하나씩 더한 값을 여러 개 늘어놓으면 레이어가 쌓인 것처럼 보입니다. 관성 lerp 곡선으로 층이 목표 두께를 살짝 지나쳤다 정착하고, 쌓일수록 blur가 걷혀 또렷해집니다.

.sh-text { text-shadow: none; filter: blur(5px); }
@keyframes sh-extrude {
  0%, 15% { text-shadow: none; filter: blur(5px); }
  55%, 85% {
    text-shadow:
      1px 1px 0 $subject-blue,
      2px 2px 0 $subject-blue,
      3px 3px 0 $subject-blue,
      4px 4px 0 $subject-blue,
      5px 5px 8px rgba(0, 0, 0, .35);
    filter: blur(0);
  }
  100% { text-shadow: none; filter: blur(5px); }
}
.sh-text.is-demo { animation: sh-extrude 2s $ease-lerp-settle infinite; }

CSS 텍스트 애니메이션은 어디서 깨지는가

채색 레이어가 테두리를 가렸다

가장 먼저 걸려 넘어진 건 08번이었습니다. 테두리만 있는 글자 위에 채색된 글자를 그냥 겹쳐만 뒀더니, 위치만 다르고 항상 다 보이는 채색 레이어가 테두리 레이어를 완전히 가려버려서 테두리에서 채워지는 과정 자체가 화면에 안 보였습니다.

clip-path 로 뒤집은 해법

겉으로는 잘 도는 것처럼 보였지만 프레임을 하나씩 열어 보니 테두리 단계가 통째로 빠져 있었던 것이죠. 채색 글자에 별도 덮개를 씌우는 대신 clip-path를 채색 글자 자신에게 직접 걸어 왼쪽부터 넓어지게 바꾸자 테두리 단계가 제대로 드러났습니다.

글자 하나하나에 그림자를 쌓다 한계를 넘기는 이 지점은 질감을 겹겹이 올리는 스큐어모피즘 랜딩 키트에서 면 단위로 같은 한계를 만납니다.

이 zip을 열 때 쓰는 비밀번호는 jagvrc78 위치에 평범한 문장으로 숨겨 뒀고, 이 글을 끝까지 읽다 보면 자연스럽게 마주치게 됩니다.

접근성 — 아홉 개가 reduced-motion에서 남기는 것

멈춘 채 최종 모습으로

동작 줄이기(reduced motion)를 켜 둔 사람에게는 아홉 개 모두 prefers-reduced-motion: reduce 아래에서 애니메이션이 멈춘 채로 보입니다.

06번만 다른 이유

01~05·07~09번처럼 SCSS @keyframes로 움직이는 항목은 최종 모습(글자가 다 드러나고, 자간이 좁혀지고, 그림자가 쌓인 상태)을 !important로 고정해 곧바로 다 보이게 했습니다. 06번 스크램블 디코드만 CSS가 아니라 자바스크립트로 움직이기 때문에, 스크립트 안에서 matchMedia로 직접 검사해 무작위로 섞지 않고 완성된 단어를 바로 보여줍니다.

더 읽을 거리

글자가 순서대로 나타나는 다른 예시가 궁금하다면 CSS 텍스트 애니메이션 효과 9종 글도 참고할 만하고, 전체 목록은 CSS 애니메이션 카테고리에서 모아 볼 수 있습니다. clip-path 문법은 MDN 문서에서 확인할 수 있습니다.

FAQ — 자주 묻는 질문

글자 애니메이션 효과에 자바스크립트가 꼭 필요한가요?

대부분은 아닙니다. 아홉 개 중 여덟 개는 SCSS @keyframes만으로 동작하고, 06번 스크램블 디코드만 글자 내용을 무작위로 바꿔야 해서 짧은 자바스크립트가 들어갑니다.

01번 단어 단위 떠오르기와 04번 줄마다 엇갈려 슬라이드 중 어느 걸 써야 하나요?

한두 줄짜리 짧은 헤드라인이라면 01번처럼 단어 단위로 쪼개는 쪽이 자연스럽고, 두세 줄로 이어지는 선언문이나 미션 문구라면 04번처럼 줄 단위로 지그재그로 들어오는 쪽이 리듬감을 더 잘 살립니다.

zip 안의 vanilla와 react 코드가 다른가요?

보이는 애니메이션은 vanilla와 React 두 판이 똑같이 재현합니다. 다만 vanilla는 is-demo 클래스로 강제로 반복 재생시키는 반면, React 컴포넌트는 실제 서비스에 맞춰 IntersectionObserver로 화면에 들어오는 순간 딱 한 번만 재생하도록 되어 있습니다. 등장 연출을 암호가 풀리는 듯한 방식으로 바꾸고 싶다면 디코드 텍스트 효과가 그 변주입니다.

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

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