本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
アイコン

いいねのハートのアイコンが弾け、粒が飛ぶ

デモ

操作:クリック。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

用途
アイコン
動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

ハートは、同じ形の塗りと線画を重ねて描きます。押すと、全体が一瞬縮んでから弾み、そのあいだに塗りが中心から膨らんで満ち、線もいいねの色に変わります。同時に同じ色の輪が広がって消え、12 個の粒が縁から外へ飛び散ります。もう一度押すと、塗りが縮んで線画に戻ります。

縮むのは 0.1 秒で 0.8 倍までです。戻りは 0.6 秒の elastic.out で弾ませ(振れ幅 1.2・周期 0.4)、塗りは 0.35 秒の back.out(2.5) で満たします。塗りが一度行き過ぎてから収まるので、中身が詰まるような手応えが出ます。輪は 0.3 倍から 1.15 倍まで広げながら薄くし、粒は等間隔の向きから ±10° ほどずらして、ハートの幅の 0.6〜0.8 倍の距離まで飛ばしながら縮めます。

塗りは SVG の中の図形なので、何も指定せずに拡大すると、中心ではなく左上を起点に膨らみ、ハートが斜めから現れます。transformOrigin を「50% 50%」にして、塗り自身の中心から膨らませます。

いいねは 1 本のタイムラインにまとめ、押すたびに頭から再生します(restart)。取り消しは別の短い動きで、塗りを縮めて線を文字の色に戻します。弾けている途中に取り消すと、2 つの動きが塗りの大きさを取り合い、取り消したあとで塗りがまた膨らみます。取り消す前に、いいねの動きを終わりまで進めて止めておきます。

押した状態は aria-pressed で伝えます。いいねの色は、くすみローズを暗くした濃い段にしています。粒は、その色からローズ・グレージュ・ラベンダーグレー・セージまでを順に回します。粒の数と色、飛ぶ距離、弾みの強さで印象が変わります。動きを減らす設定では、ほぼ一瞬で切り替えます。このデモは 24px のアイコンを 9 倍に拡大して見せています。SNS 風のいいね、お気に入り、ブックマークに向いています。

コード

HTML
<!-- その場で状態が変わるので button。状態は aria-pressed、名前は aria-label が持つ -->
<button class="like" type="button" aria-pressed="false" aria-label="いいね">
  <!-- 輪と粒は飾りなので読み上げから外す。粒は script.js が入れる -->
  <span class="like__ring" aria-hidden="true"></span>
  <span class="like__burst" aria-hidden="true"></span>
  <svg class="like__heart" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
    <!-- 同じ形の塗りと線画を重ねる。塗りを中心から膨らませて満たす -->
    <path class="like__fill" d="M12 20.6C12 20.6 3 15.2 3 8.9C3 6.1 5.1 4 7.7 4C9.5 4 11.1 5.1 12 6.6C12.9 5.1 14.5 4 16.3 4C18.9 4 21 6.1 21 8.9C21 15.2 12 20.6 12 20.6Z" />
    <path class="like__outline" d="M12 20.6C12 20.6 3 15.2 3 8.9C3 6.1 5.1 4 7.7 4C9.5 4 11.1 5.1 12 6.6C12.9 5.1 14.5 4 16.3 4C18.9 4 21 6.1 21 8.9C21 15.2 12 20.6 12 20.6Z" />
  </svg>
</button>

CSS
.like {
  --size: 24px;    /* ハートの大きさ */
  --ink: #262626;  /* 線画の色 */
  --liked: #a05a54; /* いいねの色(くすみローズの濃い段) */
  --dot: 3px;      /* 飛び散る粒の大きさ */
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 48px;     /* 押せる範囲。アイコンより広くとる */
  height: 48px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

/* 輪・粒・ハートを同じ升に重ね、どれも中央に置く */
.like > * {
  grid-area: 1 / 1;
}

.like__heart {
  width: var(--size);
  height: var(--size);
  overflow: visible;
}

.like__fill {
  fill: var(--liked);
}

/* 24 グリッドで線幅 2(24px で描けば 2px)。線端と角は丸 */
.like__outline {
  fill: none;
  stroke: var(--ink);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* 押した瞬間に広がって消える輪。ふだんは見せない */
.like__ring {
  width: calc(var(--size) * 0.92);
  height: calc(var(--size) * 0.92);
  box-sizing: border-box;
  border: 2px solid var(--liked);
  border-radius: 50%;
  opacity: 0;
}

/* 粒が飛び出す起点(中央の点) */
.like__burst {
  position: relative;
  width: 0;
  height: 0;
}

.like__dot {
  position: absolute;
  left: calc(var(--dot) / -2);
  top: calc(var(--dot) / -2);
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
}

/* 粒の色は 5 色を順に回す(いいねの色・ローズ・グレージュ・ラベンダーグレー・セージ) */
.like__dot:nth-child(5n + 1) { background: var(--liked); }
.like__dot:nth-child(5n + 2) { background: #b27771; }
.like__dot:nth-child(5n + 3) { background: #a28767; }
.like__dot:nth-child(5n + 4) { background: #877fa1; }
.like__dot:nth-child(5n) { background: #739374; }

.like:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const COUNT = 12; // 飛び散る粒の数

document.querySelectorAll(".like").forEach((button) => {
  const heart = button.querySelector(".like__heart");
  const fill = button.querySelector(".like__fill");
  const outline = button.querySelector(".like__outline");
  const ring = button.querySelector(".like__ring");
  const burst = button.querySelector(".like__burst");
  const styles = getComputedStyle(button);
  // 色と大きさは CSS が正。GSAP は var(--name) のままでは補間できないので、値にして渡す
  const ink = styles.getPropertyValue("--ink").trim();
  const likedColor = styles.getPropertyValue("--liked").trim();
  const size = parseFloat(styles.getPropertyValue("--size"));

  const like = gsap
    .timeline({ paused: true })
    // 押した瞬間に縮み、いいねの色に満ちながら弾んで戻る
    .fromTo(heart, { scale: 1 }, { scale: 0.8, duration: 0.1, ease: "power2.in" }, 0)
    .fromTo(heart, { scale: 0.8 }, { scale: 1, duration: 0.6, ease: "elastic.out(1.2, 0.4)", immediateRender: false }, 0.1)
    // SVG の中の図形は、原点を中央にしないと左上から膨らむ
    .fromTo(fill, { scale: 0, transformOrigin: "50% 50%" }, { scale: 1, duration: 0.35, ease: "back.out(2.5)" }, 0.1)
    .fromTo(outline, { stroke: ink }, { stroke: likedColor, duration: 0.12, ease: "none" }, 0.1)
    // 輪が広がりながら消える
    .fromTo(ring, { scale: 0.3, opacity: 1 }, { scale: 1.15, duration: 0.5, ease: "power2.out", immediateRender: false }, 0.1)
    .to(ring, { opacity: 0, duration: 0.35, ease: "power1.in" }, 0.25);

  // 粒は向きを等間隔から少しずらし、飛ぶ距離と大きさを散らす
  for (let i = 0; i < COUNT; i++) {
    const dot = document.createElement("span");
    dot.className = "like__dot";
    burst.append(dot);
    const angle = (i / COUNT) * Math.PI * 2 + (Math.random() - 0.5) * 0.35;
    const [x, y] = [Math.cos(angle), Math.sin(angle)];
    const far = size * (0.62 + Math.random() * 0.2);
    like
      .fromTo(dot, { scale: 0 }, { scale: 0.7 + Math.random() * 0.6, duration: 0.1, ease: "power1.out" }, 0.12)
      .fromTo(dot, { x: x * size * 0.3, y: y * size * 0.3 }, { x: x * far, y: y * far, duration: 0.6, ease: "power3.out" }, 0.12)
      .to(dot, { scale: 0, duration: 0.4, ease: "power2.in" }, 0.32);
  }

  // 動きを減らす設定では、ほぼ一瞬で切り替える
  if (reduceMotion) like.timeScale(20);

  // 最初からいいね済み(保存した状態を読み込んだときなど)は、動かさずに終わりの状態にする
  if (button.getAttribute("aria-pressed") === "true") like.progress(1);

  let undo = null; // 取り消しの動き
  button.addEventListener("click", () => {
    const liked = button.getAttribute("aria-pressed") !== "true";
    button.setAttribute("aria-pressed", String(liked));
    undo?.kill();
    if (liked) {
      like.restart(); // 押すたびに頭から
      return;
    }
    // 弾けている途中なら終わらせてから戻す(塗りの大きさを 2 本の動きで取り合わない)
    like.progress(1).pause();
    undo = gsap
      .timeline()
      .to(heart, { scale: 0.85, duration: 0.1, ease: "power2.in" })
      .to(heart, { scale: 1, duration: 0.3, ease: "power2.out" })
      .to(fill, { scale: 0, duration: 0.25, ease: "power2.in" }, 0)
      .to(outline, { stroke: ink, duration: 0.2, ease: "none" }, 0)
      .timeScale(reduceMotion ? 20 : 1);
  });
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。