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

ボタンのクリックした位置から波紋が広がる

デモ

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

仕様

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

しくみと調整

押した位置に、くすんだ青灰色(パレットのスレートの明るい段)の円を置き、広げながら薄れさせます。ボタンは、はみ出しを隠した丸帯なので、円はその形に切り取られます。円は押すたびに作って、薄れきったら消すので、続けて押すと波紋が重なって広がります。

円の半径は、押した位置からいちばん遠い角までの距離にします。こうすると、どこを押しても、広がりきった円がボタンの端まで届きます。広がるのは 0.6 秒の power2.out、薄れるのは 0.25 秒後からの 0.45 秒で、不透明度 0.5 から始めます。広がりきる前に薄れ始めるので、押した手応えだけが残り、いつまでも光りません。

円を文字の下に回すために z-index を -1 にするときは、ボタンに isolation の isolate を付けます。付けないと、円がボタンの地より後ろへ回り、何も見えなくなります。押した位置は、ボタンの画面上の大きさに対する比から、ボタンの中の座標に直します。ページごと拡大・縮小されていても、押した所から広がります。

押す部品は button にします。Enter やスペースで押したときは押した位置が無いので、真ん中から広げます。動きを減らす設定では、広げずにボタン全体を淡く光らせて消すだけにします。

明るい地のボタンでは明るい波紋が溶けるので、墨色の波紋にして、始まりの不透明度を 0.12 ほどに下げます。広がる時間を短くすると軽快に、長くすると柔らかく見えます。フォームの送信・一覧の行・カードのように、押したことを手元で確かめたい部品に向いています。

コード

HTML
<button class="ripple-button" type="button">次へ進む</button>

CSS
.ripple-button {
  --face: #262626; /* 地 */
  --face-hover: #111111; /* 重ねたときの地(少し濃く) */
  --ink: #ffffff; /* 文字 */
  --ripple: #98b1c4; /* 波紋(墨色の地の上で明るく見える、くすんだ青灰色) */
  position: relative;
  isolation: isolate; /* 波紋(z-index: -1)を、ボタンの地の上・文字の下に描く */
  overflow: hidden; /* 波紋を丸帯の形に切る */
  height: 48px;
  padding: 0 32px;
  border: 0;
  border-radius: 999px;
  background-color: var(--face);
  color: var(--ink);
  font: inherit;
  font-size: 15px; /* font: inherit の後に書く(先だと打ち消される) */
  font-weight: 600;
  letter-spacing: 0.04em;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.2s linear;
  -webkit-tap-highlight-color: transparent; /* スマホの押した色を消す(波紋と二重になる) */
}

.ripple-button:hover { background-color: var(--face-hover); }
.ripple-button:focus-visible { outline: 2px solid var(--face); outline-offset: 4px; }

/* 波紋。押すたびに JS が作り、薄れきったら消す */
.ripple-button__ink {
  position: absolute;
  z-index: -1;
  border-radius: 50%;
  background-color: var(--ripple);
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .ripple-button { transition: none; }
}

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

document.querySelectorAll(".ripple-button").forEach((button) => {
  // x と y はボタンの中の位置(px)
  const ripple = (x, y) => {
    const w = button.offsetWidth;
    const h = button.offsetHeight;
    // 半径を「押した位置からいちばん遠い角まで」にすると、どこを押してもボタンの端まで届く
    const radius = Math.hypot(Math.max(x, w - x), Math.max(y, h - y));
    const ink = document.createElement("span");
    ink.className = "ripple-button__ink";
    Object.assign(ink.style, {
      left: `${x - radius}px`,
      top: `${y - radius}px`,
      width: `${radius * 2}px`,
      height: `${radius * 2}px`,
    });
    button.prepend(ink);

    // 押すたびに作り、薄れきったら消す(続けて押しても、前の波紋が途中で消えない)
    gsap
      .timeline({ onComplete: () => ink.remove() })
      // 動きを減らす設定では広げず、ボタン全体を淡く光らせて消すだけにする
      .fromTo(ink, { scale: reduceMotion ? 1 : 0, opacity: 0.5 }, { scale: 1, duration: 0.6, ease: "power2.out" }, 0)
      .to(ink, { opacity: 0, duration: 0.45, ease: "power1.out" }, 0.25);
  };

  button.addEventListener("pointerdown", (event) => {
    if (event.button !== 0) return; // 右クリックでは出さない
    // 画面上の位置を、ボタンの中の座標に直す(ボタンが拡大・縮小されていてもずれない)
    const r = button.getBoundingClientRect();
    ripple(((event.clientX - r.left) / r.width) * button.offsetWidth, ((event.clientY - r.top) / r.height) * button.offsetHeight);
  });

  // Enter やスペースで押したときは押した位置が無い(detail が 0)ので、真ん中から広げる
  button.addEventListener("click", (event) => {
    if (event.detail === 0) ripple(button.offsetWidth / 2, button.offsetHeight / 2);
  });
});

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

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