デモ
デモを読み込めませんでした。
仕様
- 用途
- ボタン
- 動作
- 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 ほどに下げます。広がる時間を短くすると軽快に、長くすると柔らかく見えます。フォームの送信・一覧の行・カードのように、押したことを手元で確かめたい部品に向いています。
コード
<button class="ripple-button" type="button">次へ進む</button>
.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; }
}
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);
});
});