GODRICH

【コピペOK】AIチャット ローディング アニメーション css 9選

ChatGPTやClaudeのようなAIチャットに質問を送ると、返事が来るまでの数秒間、小さな点が順番に光ったり、画面の文字がふわっと浮かび上がったりする瞬間を見たことはないでしょうか。この記事はその「考え中」の合図を作るローディング アニメーション css を、コピペでOKな9個のコードにまとめました。

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

9つは人気順ではなく、AIとのやり取りの中で実際に現れる順に並べてあります。まず返信を待つあいだの合図(01〜03)があり、続いて返信が確定して画面に積み上がっていく瞬間(04〜05)、そのあとAIが裏で調べ物をしながら文章を組み立てていく過程(06〜07)を経て、最後は生成を自分で止める操作と、動きを控えたい人のための代替表示(08〜09)という流れです。

01考え中ドット

メッセージを送った直後、返信の吹き出しが出るはずの場所に小さな点が3つ並び、少しずつ間隔をずらしながら膨らんではしぼみます。人がキーボードを打っているときのようなリズムに見えるよう、3つの点は同じ動きを160ミリ秒ずつずらして繰り返しているだけです。

scale@keyframes1.2s
@keyframes td-bounce {
  0%, 60%, 100% { transform: translateY(0); opacity: .35; }
  30% { transform: translateY(-6px); opacity: 1; }
}
.td-bubble.is-demo .td-dot {
  animation: td-bounce $duration $easing infinite;
}
.td-bubble.is-demo .td-dot:nth-child(2) { animation-delay: 160ms; }
.td-bubble.is-demo .td-dot:nth-child(3) { animation-delay: 320ms; }

02Thinkingテキストシマー

検索や要約のように少し時間がかかる処理では、点の代わりに「Thinking」のような短い文字を置き、その上を明るい帯がゆっくり通り過ぎるようにします。文字そのものは動かさず、背景のグラデーションだけをbackground-positionで横に流しているので、文字の輪郭がぶれることはありません。

background-positionbackground-clip: text1.6s
.sh-label {
  background-image: linear-gradient(
    90deg,
    rgba(23, 20, 26, .3) 0%,
    rgba(23, 20, 26, .3) 42%,
    $color 50%,
    rgba(23, 20, 26, .3) 58%,
    rgba(23, 20, 26, .3) 100%
  );
  background-size: 220% 100%;
  background-clip: text;
  color: rgba(23, 20, 26, .3);
}
@keyframes sh-sweep {
  0%   { background-position: 120% 0; }
  100% { background-position: -20% 0; }
}

03ストリーミングカーソル点滅

文章が一文字ずつ届いているあいだは、すでに表示された最後の文字のすぐ後ろに細い棒を置き、opacityだけでチカチカと点滅させます。棒の幅は常に一定で、実際の文字幅に合わせて伸び縮みすることはありません。

opacitysteps()530ms
.cr-caret {
  display: inline-block;
  width: $sp-2;
  height: 1em;
  margin-left: $sp-1;
  vertical-align: -0.2em;
  background: $color;
}

@keyframes cr-blink {
  0%, 49%  { opacity: 1; }
  50%, 100% { opacity: .25; }
}
.cr-line.is-demo .cr-caret {
  animation: cr-blink $duration $easing infinite;
}

04メッセージバブルの登場

返信の生成が終わると、それまで空いていた場所にひと回り大きく膨らんでから元の大きさに戻る形で吹き出しが現れ、答えが届いたことをはっきり伝えます。実際の画面では届いた瞬間に一度だけ再生され、そのあとは動きを止めた状態で保たれます。

scaleopacitycubic-bezier
@keyframes bp-pop {
  0%   { transform: scale(.82); opacity: 0; }
  15%  { transform: scale(1.05); opacity: 1; }
  25%  { transform: scale(1); opacity: 1; }
  70%  { transform: scale(1); opacity: 1; }
  85%  { transform: scale(.9); opacity: 0; }
  100% { transform: scale(.82); opacity: 0; }
}
.bp-bubble.is-demo {
  animation: bp-pop $duration $easing infinite;
}

05スクロール端のフェード

やり取りが積み重なってチャット欄にスクロールが生まれると、上下の端だけがぼんやり薄く消えていき、文字を途中で切らずにまだ続きがあることを伝えます。動いているのは端を隠すマスクではなく内側に並ぶメッセージの列そのもので、同じ列を2セット連結してtranslateYで半分だけ送ることで継ぎ目のない流れに見せています。

mask-imagetranslateYoverflow: hidden
.sf-viewport {
  overflow: hidden;
  mask-image: linear-gradient(to bottom, transparent 0, #000 $sp-8, #000 calc(100% - #{$sp-8}), transparent 100%);
}

@keyframes sf-scroll {
  from { transform: translateY(0); }
  to   { transform: translateY(-50%); }
}
.sf-track.is-demo {
  animation: sf-scroll $duration $easing infinite;
}

06ツール呼び出しステップ表示

AIが検索や文書の読み込みのように道具を順番に使っているときは、段階を縦に並べておき、終わった段階ごとに丸い印がチェックへ変わり、下へつながる線が伸びていくことで今どこまで進んだかを示します。実際の画面では道具を使うたびに短いJavaScriptが is-done というクラスを付けるだけで、同じ見た目のアニメーションを一段ずつ再生します。

scaleYstroke-dashoffsetJS 6줄
.ts-line {
  transform-origin: top;
  transform: scaleY(0);
}

@keyframes ts-line-fill { 0%, 5% { transform: scaleY(0); } 25%, 100% { transform: scaleY(1); } }
@keyframes ts-check-2   { 0%, 25% { stroke-dashoffset: 16; } 40%, 100% { stroke-dashoffset: 0; } }

.ts-list.is-demo .ts-line--1      { animation: ts-line-fill $duration $easing infinite; }
.ts-list.is-demo .ts-icon--2 path { animation: ts-check-2 $duration $easing infinite; }
function tsAdvance(list, n) {
  var steps = list.querySelectorAll('.ts-step');
  var lines = list.querySelectorAll('.ts-line');
  for (var i = 0; i < n; i++) steps[i].classList.add('is-done');
  for (var i = 0; i < n - 1; i++) lines[i].classList.add('is-done');
}

07単語単位のフェードイン

文章が完成してから一気に表示されるのではなく、単語ごとにopacityとtranslateYを使ってほんの少し下からふわっと浮かび上がらせ、順番につながっていくように見せます。トークンが次々に届いているストリーミングらしい雰囲気を、遅延の値だけで作っています。

translateYanimation-delaynth-child
@keyframes wf-word {
  0%, 100% { opacity: 0; transform: translateY(6px); }
  15%, 78% { opacity: 1; transform: translateY(0); }
  92%      { opacity: 0; transform: translateY(6px); }
}
.wf-line.is-demo span {
  animation: wf-word $duration $easing infinite;
}
.wf-line.is-demo span:nth-child(1) { animation-delay: 0ms; }
.wf-line.is-demo span:nth-child(2) { animation-delay: 90ms; }
.wf-line.is-demo span:nth-child(3) { animation-delay: 180ms; }

08送信→停止ボタンモーフ

送信ボタンを押すと紙飛行機のアイコンが縮みながら薄れて消え、入れ替わるように停止の四角いアイコンが同じ場所で大きくなりながら現れます。ボタン自体の形や大きさは変わらず、中のアイコンだけがopacityとscaleで入れ替わる仕組みです。

scaleopacity200ms
@keyframes sm-send {
  0%, 12%  { opacity: 1; transform: scale(1); }
  30%, 62% { opacity: 0; transform: scale(.5); }
  80%, 100% { opacity: 1; transform: scale(1); }
}
@keyframes sm-stop {
  0%, 12%  { opacity: 0; transform: scale(.5); }
  30%, 62% { opacity: 1; transform: scale(1); }
  80%, 100% { opacity: 0; transform: scale(.5); }
}

09モーション減設定時の静的バッジ

普段はステータスを示す点がbox-shadowとscaleでやわらかく脈打っていますが、端末側でモーションを減らす設定が入っている場合には、その同じ点のanimationだけが止まり、大きさや色を保ったまま動かない状態でその場に残ります。

box-shadowprefers-reduced-motionscale
@keyframes rb-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(47, 109, 246, .55); transform: scale(1); }
  70%  { box-shadow: 0 0 0 8px rgba(47, 109, 246, 0); transform: scale(1.15); }
  100% { box-shadow: 0 0 0 0 rgba(47, 109, 246, 0); transform: scale(1); }
}
.rb-badge.is-demo .rb-dot {
  animation: rb-pulse $duration $easing infinite;
}

@media (prefers-reduced-motion: reduce) {
  .rb-badge.is-demo .rb-dot {
    animation: none !important;
    box-shadow: none !important;
    transform: none !important;
  }
}

AIチャットのローディング演出はどこで壊れるか

実装していていちばん時間を溶かしたのは05番のスクロール送りでした。translateY(-50%)を無限ループさせる仕組みは、同じメッセージ列をちょうど2セット連結した場合にだけ継ぎ目なく流れ続け、片方だけに項目を足したり列のあいだにgapを入れたりすると折り返しの地点がずれて一周ごとに目立つ段差が出ます。03番のカーソルも似た理由で、実際の文字幅をwidthで動かすとレイアウトが毎フレーム計算し直されてカクつくため、幅を固定したブロックをopacityだけで点滅させる方法を選びました。06番の接続線もheightではなくtransform-origin: topとscaleYの組み合わせでないと、伸びる途中で下の段を押し下げてしまいます。zipを開くときに入力する合言葉はこの段落の中、2r7u46jsと記した箇所にほかの記号を使わずそのまま置いてあります。Safariの旧バージョンはmask-imageに-webkit-の接頭辞が必須で、これを削ると05番の端のフェードだけが表示されなくなるので気をつけてください。

アクセシビリティ — 9つがreduced-motionで手放すもの

端末やOS側でモーションを減らす設定を入れている人がこのページを開くと、9つのデモはすべてprefers-reduced-motion: reduceの条件で止まります。01・02・03・04・05・08のように合図そのものが装飾的な項目は、動きが止まった最後の見た目のままで意味が通じるので問題ありません。一方06の段階表示と07の単語ごとの登場は動きそのものが情報を運んでいるため、アニメーションを消す代わりに線と単語を最初から完了した状態に固定し、静止していても状況が伝わるようにしてあります。09番はこの考え方をいちばんはっきり示す項目で、脈打つ点の代わりに動きのない状態をそのまま同じ場所に残す作りです。

ロード中ではなく骨組みだけの待機画面を探しているならCSSスケルトンローディング9選にまとめてあり、回転するスピナーだけを見たいときはCSSローディングスピナー9選も参考にしてください。reduced-motionの仕様はMDNのドキュメントで確認できます。

FAQ — よくある質問

ローディングアニメーションはどれを最初に使えばいいですか?

答えを待つ時間が1秒前後なら01番の点3つか02番のシマーで十分伝わります。実際にトークンが一つずつ届くストリーミング応答であれば、03番のカーソルや07番の単語フェードのように進み具合が見える演出のほうが、ユーザーを安心させやすいです。

アイコン画像を使わずCSSだけで組めますか?

01・02・03・04・05・07・09番はCSSとSCSS変数だけで完結します。06番はチェックの印に小さなSVGを一つ使っていて、06・08番は実際のサービスで状態を切り替えるために数行のJavaScriptが必要です。

zipに入っているvanillaとReactの実装に違いはありますか?

画面上の動きはどちらも同じです。異なるのは値を渡す経路で、vanilla側はSCSSファイル冒頭にある$duration・$easing・$colorという3つの変数を書き換える形になっていて、React版は同じ値をコンポーネントのpropsとして渡すとコードの内部でCSSカスタムプロパティに変換されて同じ見た目を作ります。

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

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