GODRICH

채팅 말풍선 CSS 애니메이션 9종 — 한 줄 복붙으로

채팅 말풍선 CSS 애니메이션 아홉 개를 한 번에 모았습니다. 팝 등장부터 입력 중 점, 읽음 표시, 이미지 로딩, 답장 인용, 이모지 반응, 날짜 구분선, 새 메시지 하단 고정까지 메시지 한 통이 화면에서 겪는 순간을 담았고, 전부 transform과 opacity만 움직입니다.

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

아홉 개는 흔한 인기순이 아니라 메시지 한 통이 겪는 순서로 놓았습니다. 도착해서 눈에 띄고(01), 발신·수신을 구분하고(02), 상대가 입력 중임을 알리고(03), 보낸 메시지가 읽혔는지 확인시키고(04), 사진이 도착하는 과정을 보여주고(05), 이전 대화의 맥락을 잇고(06), 반응을 남기고(07), 날짜가 바뀌었음을 표시하고(08), 마지막엔 새 메시지가 화면 하단에 붙습니다(09). 코드는 흐름만 보이는 조각으로 실었고, 붙여서 바로 돌아가는 완성본은 zip 하나에 다 있습니다.

01말풍선 팝 등장

새 메시지 말풍선이 scale(0.4→1)로 튀어나오듯 커지며 opacity와 함께 나타납니다. 1:1 채팅의 새 메시지나 알림 토스트 안 미리보기처럼 "방금 왔다"가 확실해야 하는 자리에 씁니다.

scale(0.8→1)opacity0.18초
.bubble {
  transform-origin: center;
  &.is-demo { animation: bubble-pop-loop $dur-loop $easing infinite; }
}
@keyframes bubble-pop-loop {
  0%   { transform: scale(.4); opacity: 0; }
  16%  { transform: scale(1.15); opacity: 1; }
  24%  { transform: scale(1); opacity: 1; }
  82%  { transform: scale(1); opacity: 1; }
  100% { transform: scale(.4); opacity: 0; }
}

02꼬리 방향 전환

말풍선 모서리의 삼각형 꼬리를 clip-path로 그리고, 보낸 사람이냐 받은 사람이냐에 따라 말풍선 위치와 꼬리 두 장의 opacity를 서로 반대로 바꿔칩니다. 발신·수신을 구분해야 하는 1:1·그룹 채팅에 씁니다.

clip-pathpolygon()좌우 반전
.tail--left  { left: -6px;  clip-path: polygon(100% 0, 100% 100%, 0 50%); }
.tail--right { right: -6px; clip-path: polygon(0 0, 0 100%, 100% 50%); }
@keyframes tail-shift-loop {
  0%   { transform: translateX(-70px); }
  32%  { transform: translateX(-70px); }
  68%  { transform: translateX(70px); }
  100% { transform: translateX(70px); }
}

03입력 중 점 세 개

점 세 개가 translateY로 하나씩 200ms 간격을 두고 튀어 오르며 상대가 지금 입력 중임을 알립니다. 메신저의 상대방 입력 상태나 챗봇의 응답 대기 표시에 씁니다.

translateYanimation-delay무한 반복
.dot {
  animation: dot-bounce-loop $dur-loop $easing infinite;
  &:nth-child(1) { animation-delay: 0s; }
  &:nth-child(2) { animation-delay: -.2s; }
  &:nth-child(3) { animation-delay: -.4s; }
}
@keyframes dot-bounce-loop {
  0%   { transform: translateY(0); opacity: .4; }
  16%  { transform: translateY(-9px); opacity: 1; }
  32%  { transform: translateY(0); opacity: .4; }
  100% { transform: translateY(0); opacity: .4; }
}

04읽음 표시 전환

안읽음 숫자 1이 상대가 읽는 순간 opacity로 사라지고 체크 아이콘이 커졌다 안착하며 그 자리를 대신합니다. 1:1 채팅의 읽음 확인이나 상담 채팅의 상담원 응답 확인에 씁니다.

opacityscale0.2초
.unread { animation: unread-out-loop $dur-loop $easing infinite; }
.check  { animation: check-in-loop $dur-loop $easing infinite; }
@keyframes check-in-loop {
  0%   { opacity: 0; transform: scale(.4); }
  48%  { opacity: 0; transform: scale(.4); }
  62%  { opacity: 1; transform: scale(1.15); }
  72%  { opacity: 1; transform: scale(1); }
  100% { opacity: 1; transform: scale(1); }
}

05이미지 말풍선 로딩

이미지가 도착하기 전엔 회색 스켈레톤 위로 하이라이트가 translateX로 훑고 지나가고, 도착하면 사진 레이어가 opacity 0에서 1로 나타나며 스켈레톤을 덮습니다. 사진·움짤을 주고받는 채팅, 상품 이미지가 붙는 문의 채팅에 씁니다.

skeleton shimmer@keyframes photo-in-loop0.3초
.shine { animation: shine-loop $dur-loop $easing infinite; }
.photo { opacity: 0; animation: photo-in-loop $dur-loop $easing infinite; }
@keyframes shine-loop {
  0%   { transform: translateX(0); }
  60%  { transform: translateX(360%); }
  100% { transform: translateX(360%); }
}
@keyframes photo-in-loop {
  0%   { opacity: 0; }
  62%  { opacity: 0; }
  85%  { opacity: 1; }
  100% { opacity: 1; }
}

06답장 인용 슬라이드

인용된 원문 조각이 왼쪽에서 translateX로 살짝 밀려 들어오며 opacity로 나타나 새 메시지 위에 붙습니다. 특정 메시지에 답장하는 스레드형 채팅이나 리뷰·댓글 답글에 씁니다.

translateXborder-left0.15초
.quote {
  border-left: 3px solid $subject-cream;
  &.is-demo { animation: quote-in-loop $dur-loop $easing infinite; }
}
@keyframes quote-in-loop {
  0%   { transform: translateX(-18px); opacity: 0; }
  22%  { transform: translateX(0); opacity: 1; }
  80%  { transform: translateX(0); opacity: 1; }
  95%  { transform: translateX(-18px); opacity: 0; }
  100% { transform: translateX(-18px); opacity: 0; }
}

07반응 이모지 팝

말풍선 모서리에 붙는 원형 배지 안 이모지가 scale(0→1.3→1)opacity로 튀어 오르며 눌린 순간을 강조합니다. 메시지에 이모지로 반응하는 채팅이나 커뮤니티 게시글의 반응 버튼에 씁니다.

scale overshootcubic-bezier0.25초
.reaction { animation: reaction-pop-loop $dur-loop $easing infinite; }
@keyframes reaction-pop-loop {
  0%   { transform: scale(0); opacity: 0; }
  55%  { transform: scale(0); opacity: 0; }
  70%  { transform: scale(1.3); opacity: 1; }
  80%  { transform: scale(1); opacity: 1; }
  96%  { transform: scale(1); opacity: 1; }
  100% { transform: scale(0); opacity: 0; }
}

08날짜 구분선 등장

날짜가 바뀌는 지점의 구분선 캡슐이 translateY(-14px→0)opacity로 위에서 살짝 내려오며 대화 목록 사이에 자리잡습니다. 여러 날에 걸친 채팅 스크롤이나 알림 목록의 날짜별 그룹에 씁니다.

opacitytranslateY(-4px)0.2초
.divider { animation: divider-in-loop $dur-loop $easing infinite; }
@keyframes divider-in-loop {
  0%   { transform: translateY(-14px) scale(.9); opacity: 0; }
  20%  { transform: translateY(0) scale(1); opacity: 1; }
  82%  { transform: translateY(0) scale(1); opacity: 1; }
  100% { transform: translateY(-14px) scale(.9); opacity: 0; }
}

09새 메시지 하단 고정

스크롤이 이미 맨 아래에 있을 때만 새 메시지가 translateY(46px→0)로 부드럽게 밀려 올라와 하단에 자리잡습니다. 실시간 채팅방의 새 메시지 수신이나 라이브 방송 채팅창에 씁니다.

@keyframes pin-in-looptranslateY(46px)조건부 자동스크롤
.bubble--out { transform-origin: bottom center; }
.bubble--out.is-demo { animation: pin-in-loop $dur-loop $easing infinite; }
@keyframes pin-in-loop {
  0%   { transform: translateY(46px); opacity: 0; }
  28%  { transform: translateY(0); opacity: 1; }
  84%  { transform: translateY(0); opacity: 1; }
  100% { transform: translateY(46px); opacity: 0; }
}

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

03 입력 중 점과 07 반응 이모지 팝을 처음 만들 때는 배지 지름을 34px로 뒀습니다. 눈으로 다시 봐도 뭐가 움직였는지 잘 안 보였는데, 채팅 UI는 원래 작은 요소가 많아서 실제 화면 크기 그대로 캔버스에 옮기면 애니메이션이 있어도 안 보이는 크기로 접히기 때문입니다. 그래서 배지를 44px로 키우고 말풍선 전체가 함께 살짝 눌렸다 돌아오도록 곁들였더니, 480×300 캔버스에서 실측한 누적면적이 03은 1.2%, 07은 3.5%까지 늘어 이 사이트가 요구하는 최소 기준(0.08%)을 여유 있게 넘겼습니다. 아홉 개가 전부 transformopacity만 움직이는 것도 같은 이유입니다 — widthtop을 바꾸면 브라우저가 매 프레임 레이아웃을 다시 계산하지만 이 둘은 합성 단계에서 끝나기 때문이고, MDN의 애니메이션 성능 가이드가 이유를 정리해 두었습니다. 02 꼬리 방향에서 쓴 clip-path는 이제 데스크톱·모바일 주요 브라우저가 접두사 없이 지원하지만, 오래된 버전까지 확인하고 싶으면 caniuse의 clip-path 지원표를 먼저 보세요. 여기 쓴 값 그대로 담아둔 아홉 개 파일은 zip 하나로 묶여 있고, 압축을 푸는 열쇠는 e469fwnd 입니다. 파일은 바로 아래 버튼에서 받습니다.

접근성

운영체제의 "동작 줄이기" 설정을 켠 사람에게는 아홉 개 모두 prefers-reduced-motion: reduce 안에서 다르게 반응합니다. 움직임만 걷어 내고 결과 상태는 그대로 보여주는 것이 원칙입니다. 01 팝 등장과 08 날짜 구분선은 뜨지 않고 바로 제자리에서 보이고, 03 입력 중 점은 흐린 점으로만 남고, 04 읽음 표시는 체크가 즉시 켜집니다. 이 설정을 어떤 브라우저가 읽는지는 MDN의 @media 문서에 정리돼 있습니다.

@media (prefers-reduced-motion: reduce) {
  .bubble.is-demo, .dot, .divider.is-demo { animation: none !important; }
}

다른 피드백·상태 애니메이션 주제는 CSS 애니메이션 카테고리에 모여 있고, 이 사이트가 무엇을 다루는지는 소개에 있습니다.

FAQ

채팅 UI가 아니라 알림·댓글창에도 쓸 수 있나요?

네. 01·04·08은 알림 토스트나 활동 로그처럼 "새로운 무언가가 나타난다"는 화면 전부에 그대로 옮겨 씁니다. 03과 07은 채팅 특유의 상태라 메신저·상담 채팅에 더 잘 맞습니다.

React에서는 클래스 토글 대신 뭘 쓰나요?

zip 안 react/ 폴더의 컴포넌트는 상태를 props 하나로 받게 만들었습니다. 04는 read, 05는 loaded처럼 불리언 값이 바뀌면 전환이 일어나고, 모듈 SCSS는 CSS 변수만 읽으므로 duration·easing·color도 그대로 props로 넘기면 됩니다.

아홉 개를 한 채팅 화면에 다 넣어도 되나요?

넣을 수는 있지만 한 메시지에는 한 가지 등장 방식이면 충분합니다. 04 읽음 표시, 07 반응, 08 날짜 구분선처럼 서로 다른 상황에서 뜨는 것들은 자연스럽게 겹치지 않습니다.

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

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