GODRICH

チャット 吹き出し css アニメーション9選 — 一行でコピペOK

チャット 吹き出し css のアニメーション9種類を、動いた状態のまま並べました。新着メッセージのポップ登場から入力中のドット、既読の切り替え、画像読み込み、返信引用、絵文字リアクション、日付区切り、新着メッセージの自動スクロールまでを実装し、動かす値はtransformとopacityの2つだけに絞っています。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

9つは効果の派手さではなく、メッセージ1通が画面上で経る順番に並べています。届いて目に留まり(01)、送信か受信かを見分けさせ(02)、相手が入力中だと知らせ(03)、送った側が読まれたかを確認させ(04)、写真が届く過程を見せ(05)、それまでの会話とつなぎ(06)、反応を残させ(07)、日付が変わったことを示し(08)、最後は新着メッセージが画面の下端に収まります(09)。コードは仕組みが分かる断片だけを載せていて、そのまま貼って動く完成版はzip1つにまとまっています。

01吹き出しポップイン

新着メッセージの吹き出しがscale(.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で左右2枚重ねて描き、送信か受信かに応じて吹き出し本体の位置としっぽの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入力中ドット

3つのドットがtranslateYで200msずつずれながら1つずつ跳ね、相手が今まさに入力中であることを知らせます。メッセンジャーの相手の入力状態や、チャットボットの応答待ち表示に使います。

translateYanimation-delay무한 반복
.dot {
  opacity: .4;

  .bubble.is-demo & {
    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で消え、チェックアイコンがscale(1.15)まで跳ねてから1に落ち着いて入れ替わります。1対1チャットの既読確認や、問い合わせチャットで担当者の返信を確かめる場面に使います。

opacityscale0.2초
.row.is-demo .unread { animation: unread-out-loop $dur-loop $easing infinite; }
.row.is-demo .check  { animation: check-in-loop $dur-loop $easing infinite; }

@keyframes unread-out-loop {
  0%   { opacity: 1; transform: scale(1); }
  48%  { opacity: 1; transform: scale(1); }
  58%  { opacity: 0; transform: scale(.4); }
  100% { opacity: 0; transform: scale(.4); }
}
@keyframes check-in-loop {
  0%   { opacity: 0; transform: scale(.4); }
  48%  { opacity: 0; transform: scale(.4); }
  62%  { opacity: 1; transform: scale(1.15); }
  100% { opacity: 1; transform: scale(1); }
}

05画像吹き出しの読み込み

画像が届く前はグレーのスケルトンの上をハイライトがtranslateXで2回なぞり、届いた瞬間は写真レイヤーがopacity0から1へ切り替わってスケルトンの上に重なります。写真やGIFをやり取りするチャット、商品画像が付く問い合わせチャットに使います。

skeleton shimmer@keyframes photo-in-loop0.3초
.shine {
  .bubble.is-demo & { animation: shine-loop $dur-loop $easing infinite; }
}
.photo {
  opacity: 0;
  .bubble.is-demo & { 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(-18px→0)でわずかに滑り込みながら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 {
  transform: scale(0);
  transform-origin: center;

  .wrap.is-demo & { 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 {
  transform-origin: center;

  &.is-demo { 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)でなめらかに押し上がって下端に収まります。zip内のJS版はこの条件をscrollTopの差分で判定し、それ以外は表示位置を動かしません。リアルタイムチャットルームの新着受信や、ライブ配信のチャット欄に使います。

@keyframes pin-in-looptranslateY(46px)조건부 자동스크롤
.bubble--out {
  transform-origin: bottom center;

  &.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; }
}

どこで崩れるか — 落とし穴

最初に踏んだ落とし穴は04の既読表示です。既読アイコンの.statusは26px四方しかなく、これは480×300のキャンバス全体からすると面積比で1割にも満たない大きさで、数字とチェックをopacityで差し替えるだけでは変化した画素がほとんど埋もれてしまいました。背後に半透明のradial-gradientでぼかした光の輪を重ね、読まれた瞬間だけscaleopacityで一度膨らませて消す.glowを足したところ、実測の累積変化面積は23.662%まで伸びました。03の入力中ドットも同じ理由で、ドット3個の上下運動だけでは面積が心もとなく、吹き出し全体をscale(1→1.06→1)でわずかに呼吸させるbubble-breathe-loopを重ねています。ここまで直した最終版の9ファイルはe469fwndという文字列がなければ開けない形でzipにまとめてあり、下のカードから取得できます。

02のしっぽで使ったclip-pathは主要ブラウザで接頭辞なしに動きますが、対応するバージョンの一覧はcaniuseのclip-path対応表で先に確認してください。9つとも動かす値をtransformopacityだけに絞っているのも同じ計測がきっかけです。この2つは合成の段階だけで完結し、ブラウザ側のレイアウト再計算を伴いません。MDNのCSS・JSアニメーション性能ガイドにもそうまとめられています。

アクセシビリティ

OSの「視差効果を減らす」設定を有効にした人には、9つともprefers-reduced-motion: reduceの中で結果の状態だけをそのまま見せます。01のポップと06の引用、08の区切り線は浮き上がらず最初から所定の位置で止まり、03のドットは薄いドットのまま静止し、04は数字を消してチェックだけを残し、07のバッジは押された後の大きさで固定されます。

@media (prefers-reduced-motion: reduce) {
  .bubble.is-demo, .dot, .divider.is-demo,
  .quote.is-demo, .row.is-demo .unread,
  .row.is-demo .check, .wrap.is-demo .reaction,
  .bubble--out.is-demo { animation: none !important; }
}

他のフィードバック・状態系アニメーションはCSSカテゴリー一覧に、このサイトが何を届けているかはサイト紹介にまとめています。

FAQ

チャットUI以外、通知や活動ログにも使えますか?

使えます。01・04・08は「何か新しいものが現れる」画面全般にそのまま流用でき、通知トーストや活動フィードのカードにも自然に馴染みます。03と07はチャット特有の状態を表すため、メッセンジャーや問い合わせチャットのほうが相性が良いです。

Reactではクラスの付け外しの代わりに何を使いますか?

zip内react/フォルダの各コンポーネントは状態をpropsで受け取る形にしてあります。04はread、05はloadedのような真偽値が切り替わるとアニメーションが始まり、モジュールSCSS側はCSS変数だけを参照するのでdurationeasingもそのままpropsで渡せます。

9つを同じチャット画面に全部置いても大丈夫ですか?

置くこと自体はできますが、1つのメッセージには1つの登場演出で十分です。04の既読、07のリアクション、08の日付区切りのようにそれぞれ別の場面で出るものは、自然に重なりません。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。