GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

css キャラクター アニメーション 9選 — かわいいキャラを動かすCSS

css キャラクター アニメーションとは、描いたキャラクターを画像ファイルなしでコードだけでまばたきさせたり跳ねさせたりする作りのことです。ハート目の顔からクッキーを分ける友だちまで9体を図形で組み、クリックするとキャラがすぐ反応するという規則ひとつで動かしました。

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

並びは難しさの順でも人気の順でもなく、キャラクターが見る人へ近づいていく順です。はじめは自分だけで表情をつくり(01〜03)、自分で現れて歩き(04・05)、2体で言葉を交わし(06)、人を待って応え(07・08)、最後に人とクッキーを分け合います(09)。9体とも骨組みは円・カプセル・三日月といった図形が数個なので、紙に描いたキャラを図形の比率へ置き換える作業が中心になります。関節のあるマスコットが手を振ったり居眠りしたりする流れは挨拶から居眠りまでの9選が、ハートをもっと大量に降らせたいときはパーティクル アニメーション canvas 9種が隣に続きます。

01ハート目のまばたき

両目がハートで埋まった丸い顔が数秒に一度、目を閉じる、ログイン完了画面に置くキャラクターです。

ハート目のまばたき
新しいタブで開く

01 · ハート目のまばたき — 計測 累積面積 2.374% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

transform-originscaleYanimation-iteration-count

まばたきは目を縦につぶして描く

.hb.is-demo .hb__eye {
  animation-name: hbBlink;
  animation-duration: $duration;
  animation-timing-function: $easing;
  animation-iteration-count: infinite;
}
@keyframes hbBlink {
  0%, 40% { transform: scaleY(1); }
  47% { transform: scaleY(.08); }
  54%, 100% { transform: scaleY(1); }
}

まぶた代わりの板を重ねる作りにすると2枚が透けて幽霊のように見えるため、目の要素そのものを scaleY(.08) まで縦につぶすやり方にし、まばたきは2秒ループの47%前後という短い瞬間だけに置きました。

ハートの鼓動と頬が空白を埋める

@keyframes hbBeat {
  0%, 12% { transform: scale(1); }
  17% { transform: scale(1.14); }
  22% { transform: scale(1); }
  27% { transform: scale(1.1); }
  32%, 100% { transform: scale(1); }
}
@keyframes hbBlush {
  0%, 61% { opacity: 0; }
  62%, 87% { opacity: .55; }
  88%, 100% { opacity: 0; }
}

まばたきの合間に生まれる長い静止を前半の鼓動2回と後半の頬の赤みが埋め、動いたフレームは 11/23・変化した面積の合計は 2.374% になり、実際に押すと aria-pressed が true に変わり、頬 .55・ハート 1.16 倍のままの静止画が残ります。

02ぷにぷにゼリーのバウンド

かがんでから縦に伸びて跳ね、着地で左右に揺れる、ゲームのスタートボタン横に置くマスコットです。

ぷにぷにゼリーのバウンド
新しいタブで開く

02 · ぷにぷにゼリーのバウンド — 計測 累積面積 12.578% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

cubic-bezierscaletranslateY

つぶれと伸びを transform ひとつで

@keyframes jlHop {
  0% { transform: translateX(-50%) scale(1, 1); }
  8% { transform: translateX(-50%) scale(1.16, .66); }
  16% { transform: translateX(-50%) translateY(-52px) scale(.9, 1.16); }
  24% { transform: translateX(-50%) translateY(-18px) scale(.98, 1.05); }
  30% { transform: translateX(-50%) scale(1.12, .78); }
  38% { transform: translateX(-50%) scale(.96, 1.06); }
  46% { transform: translateX(-50%) scale(1.06, .93); }
  54% { transform: translateX(-50%) scale(.97, 1.04); }
  62% { transform: translateX(-50%) scale(1.03, .97); }
  70%, 100% { transform: translateX(-50%) scale(1, 1); }
}

つぶれも伸びもすべて scale に載せてあり width・height には触れないので毎フレームのレイアウト計算が起きず、着地のあとの揺れが4回かけて減衰しながら 17/23 のフレームを動かします。

押すたびにもう一回り大きく跳ねる

document.querySelector('.jl__hit').addEventListener('click', function () {
  jl.classList.remove('is-demo');
  hits = hits + 1;
  count.textContent = String(hits);
  body.classList.remove('jl__body--boing');
  void body.offsetWidth;
  body.classList.add('jl__body--boing');
});

同じ要素にアニメーションのクラスが付いたままでは再生が始まらないため、いったん外して void body.offsetWidth でレイアウトを1度計算させてから付け直すと、高く跳ぶ一回きりのバウンドが最初から流れます。

03お腹をゆする笑い

三日月の目で笑いながら体が傾き、お腹が揺れる、コメント投稿成功のバッジに似合うキャラクターです。

お腹をゆする笑い
新しいタブで開く

03 · お腹をゆする笑い — 計測 累積面積 8.074% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

rotatescaleXanimation-duration

傾きの原点は足の裏に置く

.bn__bean {
  --tilt: 4deg;
  position: absolute; bottom: 4px; left: 50%; width: 52%; aspect-ratio: 1 / 1.18;
  background: $color; border: 3px solid $subject-ink;
  border-radius: 50% 50% 46% 46% / 42% 42% 58% 58%;
  transform: translateX(-50%);
  transform-origin: 50% 100%; // 付く位置: 足の裏 — 体が傾いても足はその場に残る
}

原点を体の中心に置くと、笑うたびに足が床から押し出されて浮いて見えますが、50% 100% の足の裏へ移すと体がどれだけ傾いても足は床に貼り付いたままです。

笑いの段階は変数ひとつが振れ幅

.bn[data-level="1"] .bn__bean { --tilt: 8deg; }
.bn[data-level="2"] .bn__bean { --tilt: 13deg; }
.bn[data-level="3"] .bn__bean {
  --tilt: 4deg;
  animation: bnFold 700ms cubic-bezier(.2, 1.4, .4, 1) 1 forwards;
}
@keyframes bnTilt {
  0%, 100% { transform: translateX(-50%) rotate(calc(var(--tilt) * -1)); }
  50% { transform: translateX(-50%) rotate(var(--tilt)); }
}

押すごとに data-level が上がって --tilt の値だけが入れ替わって、左右の振れが大きくなっていき、3段階目では一回きりの折れ曲がりで体が前へ倒れて 22/23 のフレームが動きました。

04落書き線のキャラの歩き

細い線で描いたキャラが自分の線を引きながら画面の外から歩いて入る、紹介ページのスクロール案内役です。

落書き線のキャラの歩き
新しいタブで開く

04 · 落書き線のキャラの歩き — 計測 累積面積 12.113% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

stroke-dashoffsetstroke-linecaptranslateX

登場は線が自分で描かれること

.dw__draw {
  fill: none; stroke: $subject-ink; stroke-width: 4;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 100; stroke-dashoffset: 0;
}
@keyframes dwDraw {
  0% { stroke-dashoffset: 100; }
  38%, 100% { stroke-dashoffset: 0; }
}

すべての path に pathLength="100" を与えて実際の長さと関係なく100にそろえたので stroke-dashoffset の値ひとつで体・脚・腕が同じ速さで描かれ、描き終わりは2秒のうち最初の38%に収まります。

脚と腕は付く位置を軸に交互に回る

// 脚・腕 — 尻・肩(付く位置)を原点に交互に振る。
// SVG の子要素の transform-origin は transform-box: view-box を明示してこそ viewBox 座標(px)で取られる
.dw__leg { transform-box: view-box; transform-origin: 200px 148px; }
.dw__arm { transform-box: view-box; transform-origin: 200px 104px; }
.dw__leg--l, .dw__arm--r { animation-name: dwSwingA; }
.dw__leg--r, .dw__arm--l { animation-name: dwSwingB; }
.dw__leg, .dw__arm {
  animation-duration: .5s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
@keyframes dwSwingA {
  0%, 100% { transform: rotate(16deg); }
  50% { transform: rotate(-16deg); }
}
@keyframes dwSwingB {
  0%, 100% { transform: rotate(-16deg); }
  50% { transform: rotate(16deg); }
}

左脚と右腕が同じキーフレームを、右脚と左腕が反対のキーフレームをたどることで歩きになり、10個のアニメーションが休みなく続いて 23/23 のフレームが動き、押すと内側の層が scaleX(-1) で裏返って反対方向へ歩き出します。

05ステッカーのポップ登場

白い縁取りのステッカーが何もないところから飛び出し、少し行き過ぎてから収まる、プロフィール飾り用の部品です。

ステッカーのポップ登場
新しいタブで開く

05 · ステッカーのポップ登場 — 計測 累積面積 27.173% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

transform-origincubic-bezierbox-shadow

0から飛び出し行き過ぎてから座る

@keyframes stPop {
  0% { transform: rotate(-10deg) scale(0); }
  14% { transform: rotate(0deg) scale(1.16); }
  24% { transform: rotate(-4deg) scale(.96); }
  32%, 58% { transform: rotate(-4deg) scale(1); }
  64% { transform: rotate(-4deg) scale(1.05); }
  72%, 100% { transform: rotate(-4deg) scale(1); }
}

14%の地点で実寸の1.16倍まで行き過ぎてから0.96倍までわずかに沈む往復が貼り付いた感触をつくり、登場からウィンクまで休む区間が短いおかげで変化した面積の合計は 27.173%、1枚あたりの最大でも 23.256% を記録しました。

表情の交代は1フレームのカットで

.st__eyeWink { border-top: 4px solid $subject-ink; border-radius: 50%; visibility: hidden; }
.st__eyeWink--l { left: 26%; }
.st__eyeWink--r { right: 26%; }
.st--wink .st__eyeOpen--r { visibility: hidden; }
.st--wink .st__eyeWink--r { visibility: visible; }
sticker.classList.toggle('st--wink', hits % 5 === 0 && hits > 0);
sticker.classList.remove('st--nudge');
void sticker.offsetWidth;
sticker.classList.add('st--nudge');

開いた目とウィンクの目を2組そろえて同じ位置に重ね、見えるほうを visibility で選ぶので5回目のクリックで表情が一度に入れ替わり、フェードに替えると2つの表情が半透明のまま1枚に重なって写ります。

06吹き出しの交代会話

向かい合う2体のキャラが吹き出しをやり取りする、サポートチャットの最初の挨拶に置く組み合わせです。

吹き出しの交代会話
新しいタブで開く

06 · 吹き出しの交代会話 — 計測 累積面積 27.268% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scaleYscaleanimation-delay

しっぽが先に出てから体が伸びる

.ch.is-demo .ch__tail { animation-name: chTail; animation-duration: $duration; animation-delay: -60ms; animation-timing-function: $easing; animation-iteration-count: infinite; }
@keyframes chPopL {
  0% { transform: scale(0); }
  10% { transform: scale(1.08); }
  16%, 44% { transform: scale(1); }
  45%, 100% { transform: scale(0); }
}

しっぽのキーフレームに負の animation-delay: -60ms を与えて同じ2秒ループを体より一歩先に回し、吹き出しの transform-origin をしっぽが触れる下の角へ置いたのでそこから伸び広がります。

押すと自分の吹き出しが固定される

.ch.mine .ch__bubble--l { animation: none; transform: scale(1); }
.ch.mine .ch__bubble--r { animation: none; transform: scale(0); }
document.querySelector('.ch__hit').addEventListener('click', function () {
  ch.classList.remove('is-demo');
  var on = ch.classList.toggle('mine');
  this.setAttribute('aria-pressed', String(on));
});

押した瞬間にプレビューのループを止め、.mine クラスが左の吹き出しだけを開いたまま留めるので状態は aria-pressed の1か所に集まり、測定では動いたフレーム 16/23・変化した面積の合計 27.268% でした。

07しっぽを振る待ち犬

その場を守りながらしっぽを振り耳をゆらす、アップロード待ち画面の相棒です。

しっぽを振る待ち犬
新しいタブで開く

07 · しっぽを振る待ち犬 — 計測 累積面積 4.646% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

rotatetransform-originanimation-timing-function

しっぽ・耳・頭はそれぞれ付く位置が軸

.dg__tail {
  position: absolute; bottom: 38%; right: 16%; width: 6%; height: 24%;
  background: $subject-pink; border: 3px solid $subject-ink; border-radius: $r-pill;
  transform-origin: 50% 100%;
  animation-name: dgWag;
  animation-duration: .5s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
@keyframes dgWag {
  0%, 100% { transform: rotate(-22deg); }
  50% { transform: rotate(22deg); }
}

しっぽの軸は体に付く下の根元、耳の軸は頭にぶら下がる上端、頭の軸は首と、3つがそれぞれ別の点で揺れながら2秒のあいだ途切れないので動いたフレームは 23/23 で埋まりました。

なでるほどしっぽが速くなる

.dg[data-speed="2"] .dg__tail { animation-duration: .36s; }
.dg[data-speed="3"] .dg__tail { animation-duration: .26s; }
pets = pets + 1;
dg.dataset.speed = String(Math.min(1 + Math.floor(pets / 3), 3));
dg.classList.toggle('dg--love', pets === 5);

3回なでるごとにしっぽの周期が 0.5s から 0.36s・0.26s へ短くなり、振れ幅は ±22度 のまま往復だけが速くなって、5回目には目が三日月へカットで替わってハートが頭の上に1つ浮かびます。

08きらめく目とハートの粒

目の中で星が回り、その周りにハートが咲く、いいねの反応画面に置くキャラクターです。

きらめく目とハートの粒
新しいタブで開く

08 · きらめく目とハートの粒 — 計測 累積面積 4.313% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateYscaleanimation-fill-mode

プレビューのハート3つは時間差で咲く

.sp__auto {
  animation-name: spRise;
  animation-duration: $duration;
  animation-timing-function: $easing;
  animation-iteration-count: infinite;
  animation-fill-mode: backwards;
}
.sp__auto--1 { left: 30%; bottom: 34%; animation-delay: 0s; }
.sp__auto--2 { left: 56%; bottom: 30%; animation-delay: .66s; }
.sp__auto--3 { left: 42%; bottom: 44%; animation-delay: 1.32s; }

animation-fill-mode: backwards が遅延の待ち時間に姿を見せないよう押さえ、0.66秒ずつずらしたハート3つが2秒の間を隙間なく埋めていくので動いたフレームは 23/23 になります。

ハートは押した場所から咲く

var rect = scene.getBoundingClientRect();
var heart = document.createElement('span');
heart.className = 'sp__fly';
heart.setAttribute('aria-hidden', 'true');
heart.innerHTML = HEART_SVG;
heart.style.left = ((e.clientX || rect.left + rect.width / 2) - rect.left) + 'px';
heart.style.top = ((e.clientY || rect.top + rect.height / 2) - rect.top) + 'px';
scene.appendChild(heart);
heart.addEventListener('animationend', function () { heart.remove(); });

押した点の画面座標をステージ基準へ換算してその場所にハートを置き、animationend で片付けるため、ハートは大きくなってから縮んで消え半透明の残像をステージに残しません。

09クッキーをひと口ずつ分け合う

押すたびにクッキーが4段階に減って頬が膨らみ、食べ終わると残念がる、1日4回のチェックイン画面用のキャラクターです。

クッキーをひと口ずつ分け合う
新しいタブで開く

09 · クッキーをひと口ずつ分け合う — 計測 累積面積 11.737% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

clip-pathscaletranslateY

ひと口は円を切り取ること

.ck__cookie {
  position: absolute; inset: 0;
  background: $subject-cream; border: 5px solid $subject-orange-deep; border-radius: 50%;
  clip-path: circle(50% at 50% 50%);
}
.ck[data-bite="1"] .ck__cookie { clip-path: circle(44% at 60% 38%); }
.ck[data-bite="2"] .ck__cookie { clip-path: circle(36% at 66% 44%); }
.ck[data-bite="3"] .ck__cookie { clip-path: circle(26% at 70% 52%); }
.ck[data-bite="4"] .ck__cookie { clip-path: circle(15% at 72% 58%); }

clip-path: circle() の半径を縮めながら中心を右上へ寄せるほど左下に三日月だけが残って噛み跡として読め、プレビューでは steps(1, end) により丸ごとから3口まで20%間隔でカットされます。

頬はひと口ごとに変数ひとつで膨らむ

.ck__cheek {
  position: absolute; top: 46%; width: 22%; aspect-ratio: 1;
  background: $subject-pink; border-radius: 50%; opacity: .75;
  transform: scale(calc(1 + var(--puff)));
  transform-origin: 50% 50%;
}
.ck[data-bite="1"] .ck__char { --puff: .05; }
.ck[data-bite="2"] .ck__char { --puff: .1; }
.ck[data-bite="3"] .ck__char, .ck[data-bite="4"] .ck__char { --puff: .16; }

--puff の値がひと口ごとに .05・.1・.16 と増えて calc を通り頬の大きさにそのまま渡り、落ちるかけら3つがフレームの隙間を埋めて変化した面積の合計を 11.737% に保ちました。

css キャラクター 紹介ページのどこに置くか?

まず効くのは、作業が終わったことをもう一度確かめさせる場所です。ログイン完了のハート目(01)、コメント投稿成功の笑い(03)、飾りを貼れたときのポップ(05)のように、成功画面のキャラクターは文言なしでも処理が済んだことを顔で伝えます。かわいい系のイラストを別に描き起こさなくても、図形数個の表情の変化が同じ役目を果たすので、css アニメーション かわいい という方向の演出は素材の準備ではなく座標の設計から始められます。

2つ目は待ち時間をなだめる場所です。アップロード中の犬(07)はしっぽの速さで進み具合を伝え、開始前のゼリー(02)は揺れで緊張をほぐします。進捗バーの横にパーセント表示を添える代わりに、キャラクターがだんだん嬉しそうになる姿で同じ情報を渡す形です。

置く場所を選ぶときは、動きの量も手がかりになります。変化した面積の合計を測ると、登場から表情の交代まで休まないステッカー(05)の 27.173% と吹き出し(06)の 27.268% が最も賑やかで、静かに瞬くだけのハート目(01)は 2.374%、粒が小さく上がるだけのきらめく目(08)は 4.313% と控えめです。読ませたい文章の隣には静かな側を、何も起きていない空白の画面には賑やかな側を置くと、キャラクターが読む邪魔をしません。

3つ目は人とやり取りする場所です。スクロール案内の歩き(04)、相談の挨拶の吹き出し(06)、いいねのハート(08)、1日4回のチェックインのクッキー(09)では、キャラクターが画面の飾りではなく会話の相手として出てきます。ただし1画面に1体が原則で、9体を1ページに並べるとかわいさが騒がしさに変わり、そのページが何のページなのかがぼやけます。

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

まばたきだけでは代表画像が止まった顔になる

まばたきは2秒のうち40〜54%という短い区間なので、それだけを置くと24枚のサンプルのほとんどが目を開けたまま止まった顔になります。01 は前半にハートの鼓動2回を、後半に頬の赤みを入れてフレームを埋め、その結果が動いたフレーム 11/23 です。それでも変化した面積の合計は 2.374% と9体の中でいちばん小さく、顔だけを動かす部品は数字の上では静かに見えるので、賑やかさを面積で比べるときは同じ土俵で比べないほうが判断を誤りません。

原点がずれるとキャラが床を滑る

笑う豆(03)の傾きの原点を体の中心に置くと、笑うたびに足が床から押し出されて浮いて見えます。足の裏へ移す1行で解決し、犬(07)のしっぽ・耳・頭も根元・ぶら下がる上端・首へそれぞれ原点を置いてはじめて自然に揺れ、SVG の中の脚(04)は transform-box: view-box を添えてこそ軸が viewBox 座標で取られます。

フェードで消すと半透明の幽霊が残る

表情の交代(05)と吹き出しの消滅(06)を opacity に任せると中間フレームがすべて半透明になり、2つの表情・2つの吹き出しが1枚に重なって写ります。目は2組を重ねて visibility のカットで選び、ハート(07・08)とかけら(09)はフェードではなく大きさを0まで縮めて消しました。9体をまとめた圧縮ファイルのパスワードは v8539tgs で、ここに挙げた直し方はすべてその中のソースに入っています。

3つのバリエーションとアクセシビリティ

変数3つを書き換えると、9体の性格がまとめて変わります。

名前 書き換える値 変わる感じ
duration $dur-loop 2s → 1s まばたきと揺れが2倍速くなり、臆病なキャラに見える
easing $ease-pop → $ease-spring 跳ねる勢いが減り、滑るように収まる
color $subject-pink → $subject-mint キャンディピンクのキャラが爽やかなミントに着替える

9体とも押す範囲は本物の <button> で aria-label を付けてあり、押された状態は 01・06 が aria-pressed、残りは data-level・data-speed・data-bite の属性1つで知らせます。回数を数えるゼリー・ステッカー・ハートのカウンターは飾りなので aria-hidden にし、回数の知らせはボタンのラベルが代わりに伝えます。prefers-reduced-motion: reduce では9つのループがすべて止まり、目を開けたハートの顔・描き終わった歩きのキャラ・座って待つ犬のように最も読める静止画が1枚ずつ残り、そのブロックの宣言には .is-demo のループ側の詳細度に勝てるよう !important を付けてあります。画面に見えるものの材料は文字と図形だけなので、受け取ったファイルを開けばブラウザでそのまま動きます。

隣の一歩は挨拶から居眠りまでの9選が関節のあるキャラクターを、パーティクル アニメーション canvas 9種が舞台いっぱいの粒を扱います。原点の計算は MDN の transform-origin の解説、跳ねる感じの数値は animation-timing-function の解説、自分で描かれる線は stroke-dashoffset の解説、クッキーの噛み跡は clip-path の解説、押された知らせは aria-pressed の解説に根拠があります。

FAQ

css アニメーション かわいい と検索して出てくる作例のように、絵が上手でないと作れませんか

いいえ、01 の顔は円1つにハート2つ・頬2つ・半円の口1つを載せただけです。三日月の目は border-top だけを残した円弧という図形の組み合わせなので描く技術は必要なく、配置の比率を数%動かすだけで表情が変わります。

キャラクターを gif や動画で入れるのと何が違いますか

gif は押せません。9体はクリックごとに段階・表情・粒で応え、画面の幅や動きを減らす設定に合わせて止めることもコードの側でできます。画像ファイルが1枚もないので、追加で読み込む重さは0です。

キャラクターを何体も置くとページが遅くなりませんか

動きがすべて transform と opacity だけでできているのでレイアウトの計算が走りません。ゼリーの揺れを width・height で作れば毎フレーム計算が起きますが、9体はそのコストを払わない作りです。ただし1画面に1体を守るほうが、速度よりも先に効きます。

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

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