デモ
デモを読み込めませんでした。
仕様
- 用途
- ボタン
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
押した瞬間にボタンを横へ広げながら縦につぶし、離したらバネのように弾ませて元の大きさに戻します。変形の支点(transform-origin)をボタンの下端の中央に置くので、下の辺は動かずに上の辺だけが沈み、床に押しつけられたように見えます。
つぶすのは押した瞬間の 0.12 秒で、横 1.1 倍・縦 0.8 倍にします。横と縦の倍率を掛けた値を 0.88 ほどに保つと、中身が詰まったまま形だけ変わったように見えます。戻すのは 0.9 秒の elastic.out で、1 倍を行き過ぎては戻るので、つぶれる → 縦に伸びる → 落ち着く、と弾みます。
押したままカーソルをボタンの外へずらして離すと、ボタンには pointerup が届かず、つぶれたまま残ります。pointerleave でも戻すようにします。キーボードでは、Enter かスペースを押しているあいだつぶし、キーを離すか焦点が外れたら戻します。
厚みは、ぼかしの無い影を地の墨より暗い色で 4px 下にずらして描きます。影もボタンと一緒に縮むので、つぶれたときは厚みも薄くなり、押し込まれたように見えます。動きを減らす設定では、弾ませずに一瞬でつぶして戻すだけにします。
縦の倍率を 0.9 に近づけると控えめになり、elastic の 2 つ目の値(0.35)を大きくすると弾みがゆったりになります。カートに入れる・いいね・ゲーム風の画面のように、押した手応えを大きく見せたいボタンに向いています。
コード
<button class="squash-button" type="button">カートに入れる</button>
.squash-button {
--face: #262626; /* 地 */
--on-face: #ffffff; /* 文字 */
--base: #000000; /* 下にのぞく厚み。地より暗くする */
--depth: 4px; /* 厚みの高さ */
height: 48px;
padding: 0 32px;
border: 0;
border-radius: 999px;
background-color: var(--face);
box-shadow: 0 var(--depth) 0 var(--base); /* ぼかさない影を下にずらして、厚みに見せる */
color: var(--on-face);
font: inherit;
font-size: 15px; /* font: inherit の後に書く(先だと打ち消される) */
font-weight: 600;
letter-spacing: 0.04em;
white-space: nowrap;
cursor: pointer;
user-select: none; /* 長押しで文字が選ばれないようにする */
touch-action: manipulation; /* ダブルタップの拡大を待たずに押せる */
-webkit-tap-highlight-color: transparent;
}
.squash-button:focus-visible {
outline: 2px solid var(--face);
outline-offset: 6px; /* 下の厚みと重ならない所に引く */
}
import { gsap } from "gsap";
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// 動きを減らす設定では、弾ませずに一瞬でつぶして戻す
const time = (seconds) => (reduceMotion ? 0 : seconds);
document.querySelectorAll(".squash-button").forEach((button) => {
// 下端を支点にすると、下の辺は動かず、床に押しつけられたようにつぶれる
gsap.set(button, { transformOrigin: "50% 100%" });
let pressed = false;
// 横に広げて縦につぶす。横と縦の倍率を掛けて 0.88 ほどにすると、中身が詰まって見える
const squash = () => {
if (pressed) return;
pressed = true;
gsap.to(button, { scaleX: 1.1, scaleY: 0.8, duration: time(0.12), ease: "power2.out", overwrite: true });
};
// elastic が 1 を行き過ぎるので、縦に伸びてから落ち着く
const release = () => {
if (!pressed) return;
pressed = false;
gsap.to(button, { scaleX: 1, scaleY: 1, duration: time(0.9), ease: "elastic.out(1.1, 0.35)", overwrite: true });
};
button.addEventListener("pointerdown", (event) => {
if (event.button === 0) squash(); // 右クリックではつぶさない
});
button.addEventListener("pointerup", release);
// 押したまま外へ出て離すと pointerup が届かないので、出たときにも戻す
button.addEventListener("pointerleave", release);
button.addEventListener("pointercancel", release);
// キーボードでは、Enter かスペースを押しているあいだつぶす
const isPressKey = (event) => event.key === "Enter" || event.key === " ";
button.addEventListener("keydown", (event) => {
if (isPressKey(event) && !event.repeat) squash();
});
button.addEventListener("keyup", (event) => {
if (isPressKey(event)) release();
});
button.addEventListener("blur", release);
});