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

アイコン

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

デモ

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

仕様

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

しくみと調整

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

塗りは0.35秒の back.out(2.5) で膨らみ、一度行き過ぎてから収まります。この行き過ぎがあるので、中身が詰まるような手応えが出ます。全体の弾みは0.6秒の elastic.out。粒は等間隔の向きから少しずつずらし、飛ぶ距離にもばらつきを持たせました。

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

いいねの動きはタイムラインにまとめ、押すたびに restart で頭から再生します。取り消しのほうは、塗りを縮めて線の色を戻す別の短い動きです。厄介なのは、弾けている途中で取り消されたとき。両方の動きが塗りの大きさを取り合い、取り消したはずの塗りがあとからまた膨らんでしまいます。取り消す前に、いいねの動きを最後まで進めて止めておくことにしました。

押されているかどうかは aria-pressed で伝えます。いいねの色はローズカラーを暗くした濃い色で、粒はそこからローズカラー、グレージュ、ラベンダー、セージへと順に色を回しています。

コード

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 サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。