GODRICH

CSS ハート アニメーション 9選 — 一行でコピペOK

css ハート アニメーションを9種類まとめました。いいねボタンのハートから星評価、拍手カウントや絵文字リアクションまで、押した瞬間に画面がはっきり反応するものだけを選び、ほとんどはtransformとopacityだけで動き、05だけwidthで割合を示します。

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

並べた順は人気ではなく、押した直後に何が起きるかです。まずハート自体が跳ねる2つ(01・02)、隣の数字が連動する1つ(03)、値を選ぶ星評価2つ(04・05)、押すたびに増える1つ(06)、あとで見返すための保存1つ(07)、確認の合図1つ(08)、最後は複数から1つを選ぶリアクション(09)という並びです。コードは動きを作る核だけを抜き出していて、完成した9ファイルはzipを開かないと出てきません。

01ハートがポンと膨らむ

いいねのラベルを押すと、内側に重ねた塗りつぶしハートがopacity 0・scale .4から1倍まで一気に伸び、回転も-8度から0度へ戻ります。裏にあるのは本物のチェックボックスなので、ページを読み込み直しても押した状態が消えません。投稿のいいねボタンや商品のお気に入りアイコンに向いています。

::before/::after 하트:checked0.22초
.heart__shape--fill {
  color: $color;
  opacity: 0;
  transform: scale(.4) rotate(-8deg);
  transition: opacity $duration $easing, transform $duration $easing;
}
.heart__input:checked ~ .heart__icon .heart__shape--fill {
  opacity: 1;
  transform: scale(1) rotate(0deg);
}
@keyframes heart-pop-loop {
  35%      { opacity: 1; transform: scale(1.15) rotate(6deg); }
  55%, 80% { opacity: 1; transform: scale(1) rotate(0deg); }
}

02パーティクルが弾ける

チェックが入った瞬間、ハートの周りに置いた8つの点がCSSカスタムプロパティ--iで角度を割り振られ、それぞれ違う向きへtranslateYとopacityで飛び散って消えます。画像は一枚も使わず、span8個とclip-pathで描いた小さなハートだけで組みました。SNSのリアクションボタンや、アプリで最初のいいねを促す場面に向いています。

--i 커스텀 속성translate+opacity스태거
.burst__dot {
  position: absolute; top: 50%; left: 50%;
  width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px;
  border-radius: 50%; background: $color;
  transform: rotate(calc(var(--i) * 45deg)) translateY(0) scale(1);
}
.burst__input:checked ~ .burst__icon .burst__dot {
  animation: particle-fly $duration $easing forwards;
  animation-delay: calc(var(--i) * 20ms);
}
@keyframes particle-fly {
  100% { opacity: 0; transform: rotate(calc(var(--i) * 45deg)) translateY(-34px) scale(.2); }
}

03いいねカウント連動

チェックのオン・オフが切り替わるたび、change イベントを拾ったJS数行が隣の数字を1増減させ、変わった直後の数字だけscaleでポンと弾みます。CSSだけでは文字そのものを書き換えられないため、ここだけJSに任せています。

JS 카운트scale pop:checked
var w = document.querySelector(".like"), inp = w.querySelector(".like__input"), num = w.querySelector(".like__num");
function sync() {
  num.textContent = inp.checked ? "13" : "12";
  num.classList.remove("is-pop"); void num.offsetWidth; num.classList.add("is-pop");
}
inp.addEventListener("change", function () { w.classList.remove("is-demo"); sync(); });

04星の塗りつぶし

5個のラジオを5・4・3・2・1の逆順に並べ、チェックされたラジオより後ろの星をすべて~セレクタで塗りつぶし、選んだ星だけさらにscaleで弾ませて選択を伝えます。5つの星はfieldsetlegendでひとつの評価グループにまとめてあります。レビューの星評価入力や商品の満足度評価に向いています。

~ 형제 선택자fieldset/legendscale 바운스
.rating__stars { display: flex; flex-direction: row-reverse; }
.rating__input:checked ~ .rating__star { color: $color; }
.rating__input:checked + .rating__star { animation: star-pick $duration $easing; }
@keyframes star-pick {
  45%  { transform: scale(1.35); }
  100% { transform: scale(1); }
}

05半星の幅クリップ表示

これは入力ではなく、平均評価だけを示す読み取り専用バッジです。同じ星5個を2枚重ね、上の層だけをwidthでカスタムプロパティ--ratingから計算した割合まで見せることで、小数点付きの評価を滑らかに塗り分けています。商品一覧の平均評価やレビュー要約バッジに向いています。

--rating 커스텀 속성overflow 폭 클리핑루프 리빌
.avg__fill {
  position: absolute; inset: 0;
  width: 0%; overflow: hidden; color: $color;
}
@keyframes reveal-fill {
  55%, 80% { width: var(--pct); }
  100%     { width: var(--pct); }
}
.avg__fill { animation: reveal-fill $dur-loop $easing infinite; }

06拍手カウント

押すたびに数字が増えるボタンで、クリックごとにアイコンがscaleとrotateで軽く弾み、「+1」の文字がtranslateYで上へ流れながらopacityで消えます。Medium風の拍手ボタンやフィードバックウィジェットに向いています。

반복 클릭translateY+opacityJS 카운트
@keyframes clap-bounce {
  40%  { transform: scale(1.3) rotate(-12deg); }
  100% { transform: scale(1) rotate(0deg); }
}
@keyframes clap-plus {
  0%   { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-16px); }
}

07ブックマークの折り込み

チェックするとリボン型のブックマークが上からscaleYで塗りつぶされ、右上角の小さな三角形がその角を基準にrotateとscaleで開き、紙が折れて戻ったように見えます。記事の保存ボタンやあとで読むリストへの追加に向いています。

clip-path 리본transform-origin 모서리rotate+scale
.bookmark__fold {
  clip-path: polygon(100% 0, 0 0, 100% 100%);
  transform: scale(0) rotate(-90deg);
  transform-origin: 100% 0;
  opacity: 0;
  transition: opacity $duration $easing, transform $duration $easing;
}
.bookmark__input:checked ~ .bookmark__icon .bookmark__fold {
  opacity: 1; transform: scale(1) rotate(0deg);
}

08親指アイコンのバウンス

チェックされる前はfilter: grayscaleで色を落としてあり、チェックされると色が戻ると同時に、親指のアイコンが-20度から5度を経て0度に戻るまでscaleも一緒に伸び縮みします。回答の「役に立った」ボタンやレビューの「参考になった」表示に向いています。

rotate+scale:checked손짓 이징
.thumb__icon {
  filter: grayscale(1) opacity(.55);
  transition: filter $duration $easing;
}
.thumb__input:checked ~ .thumb__icon {
  filter: grayscale(0) opacity(1);
  animation: thumb-bounce $duration $easing;
}
@keyframes thumb-bounce {
  30%  { transform: scale(1.25) rotate(-20deg); }
  60%  { transform: scale(1.1) rotate(5deg); }
}

09絵文字リアクション

4つの絵文字はラジオで1つだけ選べるグループにまとめ、:has()でグループ内に選んだものがあるかを判定して全体をわずかに沈め、選んだ絵文字だけopacityとscaleで再び持ち上げます。チャットメッセージのリアクションや投稿への感情リアクションに向いています。

fieldset/radio:has()scale+opacity
.react__emoji {
  transition: opacity $duration $easing, transform $duration $easing;
}
.react:has(.react__input:checked) .react__emoji {
  opacity: .4; transform: scale(.9);
}
.react__input:checked + .react__emoji {
  opacity: 1; transform: scale(1.3);
}

どこで壊れるか — カスタムプロパティの罠

05の平均評価バッジを最初に組んだときは、--fill: 0%から--fill: var(--pct)へ動かすキーフレームを、カスタムプロパティそのものに対して書いていました。ところが実際に計測すると24フレーム中たった1フレームしか値が変わっておらず、星は滑らかに満ちるどころか一瞬で切り替わっているだけでした。MDNの@propertyのページにあるとおり、カスタムプロパティは型を登録しない限り補間の対象になりません。型を登録せずに直す方法は、widthのようにもとから補間される標準プロパティへcalc(var(--rating) / 5 * 100%)という計算値を渡すことで、05はこの形に書き直してから24フレーム中13フレームが動く滑らかなアニメーションになりました。

もう一つ、04の星の塗りつぶしと09の絵文字リアクションは:has()という比較的新しいセレクタに頼っています。対応状況はcaniuse.comの:has()のページにまとまっていて、ChromeとEdgeは105以降、Safariは15.4以降、Firefoxは121以降で動き、それより古いブラウザでは09の沈み込みだけが起きず、絵文字を選ぶ操作自体は変わらず機能します。ここまで直した9つのファイルを開くには8ayu345cという文字列をそのまま入力する必要があり、下のカードからzipを受け取ってください。

アクセシビリティ

9つともprefers-reduced-motion: reduceでは跳ねたり弾けたりする動きを止め、チェックや選択という最終的な状態の色だけを残します。01・02・07・08は登場のアニメーションをやめてチェック済みの最終形をそのまま出し、05はリビール用のanimationを切って目標のwidthを最初から固定します。

@media (prefers-reduced-motion: reduce) {
  .heart__shape--fill { transition: opacity 1ms; }
  .avg__fill { animation: none !important; width: var(--pct); }
  .thumb__icon { transition: filter 1ms; }
}

04と09は星や絵文字をそれぞれfieldsetlegendでひとつのグループとしてまとめ、各ラジオに読み上げ用のaria-labelを添えています。

<fieldset class="rating">
  <legend class="rating__legend">評価</legend>
  <input type="radio" name="rating" id="r5" class="rating__input" aria-label="5点">
  <label for="r5" class="rating__star" aria-label="5点">★</label>
</fieldset>

チェックボックスの代わりにボタンで同じ状態を作るなら、押すたびに値をJSで書き換えるaria-pressed属性が必要です。

<button type="button" class="thumb" aria-pressed="false">
  <span class="thumb__icon" aria-hidden="true">👍</span>
</button>

06の拍手ボタンは押すたびに増える数字をaria-live="polite"で知らせ、画面を見ていない人にも変化が届くようにしています。

ハートや星以外のCSSアニメーションを探すならCSS記事の一覧に、このブログが何を配っているサイトかはこのサイトについてにまとめてあります。

FAQ

9つとも同じ画面に一緒に置いても大丈夫ですか?

カードごとに繰り返すいいねハートや星評価は、一覧の中に何個並んでいても問題ありません。ただし06の拍手カウントや09の絵文字リアクションのように押すたびに画面が大きく反応するものは、同じ画面に1つだけ置くほうが視線が散りません。複数が同時に弾むと、どこを見ればいいか分からなくなります。

Reactのコンポーネントでも同じ動きになりますか?

zip内のreact/以下にある各コンポーネントは、チェックや選択の状態をuseStateで持ち、色や速さを決める値だけをCSS側の変数へ渡しています。04の星評価と09のリアクションは選んだ値をonChangeで親コンポーネントへ返し、05は入力ではなくratingというprop一つで表示だけを受け取る読み取り専用の作りです。

チェックボックスをbuttonに置き換えても動きますか?

はい、オン・オフの2状態だけならaria-pressedを持つbuttonに置き換えても同じ見た目を作れます。ただしラベルと自動でつながるチェックボックスと違い、buttonは状態が変わるたびにaria-pressedの値を自分でJSから更新しないと、スクリーンリーダーには変化が伝わりません。

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

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