デモ
デモを読み込めませんでした。
仕様
- 用途
- ボタン
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
ポインタを近づけると、触れる前からボタンがそちらへ寄ってきて、中の文字はさらに一歩先へずれます。ボタンはひと回り大きい透明な要素で包んであり、その中でポインタが動くと、中心からポインタまでの距離の0.4倍だけボタンを、0.15倍だけ文字を同じ向きへずらしています。文字はボタンと一緒に動いたうえでさらに進むので、ボタンの上に少し浮いて見えます。
包む要素は、ボタンの外側へ高さの0.6倍だけ広げました。ボタンとぴったり同じ大きさだと、ポインタが触れた瞬間にいきなり寄ってきます。周りに余白があれば、近づいていく間に少しずつ寄り始めます。追従は0.5秒の power3.out で、外へ出たら elastic.out で中央へ戻すので、少し行き過ぎてから揺り戻します。
ポインタが動くたびに新しいトゥイーンを作るので、overwrite を「auto」にして、同じ値を動かしている古いトゥイーンを止めています。これを忘れると、重なったトゥイーンが同じ位置を取り合い、ボタンが手の動きから遅れて重たくついてくるようになります。タッチ端末では画面に触れずにポインタを動かせないので、ホバーができる環境でだけ有効にしています。
ボタンの比率を上げるほどポインタの近くまで寄り、文字の比率を上げるほど奥行きが強く出ます。この動きが映えるのは、ヒーローのメインボタンやフッターの「お問い合わせ」のように、1つだけ置いて目を引きたいボタンです。並んだボタンのすべてに付けると、ポインタが通るたびにどれもが反応して、画面が落ち着きません。
コード
<!-- 外側の要素が吸い寄せの領域。ボタンより広いので、近づいた時点で反応できる -->
<div class="magnetic">
<a class="magnetic__button" href="/contact/">
<span class="magnetic__label">お問い合わせ</span>
</a>
</div>
.magnetic {
/* ボタンの外へ広げる量。ボタンの高さの 0.6 倍ほどにすると、自然に近づける */
--zone: 28px;
display: inline-block;
padding: var(--zone);
}
.magnetic__button {
--ink: #eeece6; /* 背景色 */
--ground: #111111; /* 文字の色 */
display: inline-grid;
place-items: center;
height: 48px;
padding: 0 32px;
border-radius: 999px;
background-color: var(--ink);
color: var(--ground);
font-size: 15px;
font-weight: 600;
letter-spacing: 0.04em;
text-decoration: none;
white-space: nowrap;
}
.magnetic__label {
display: block;
}
.magnetic__button:focus-visible {
outline: 2px solid var(--ink);
outline-offset: 4px;
}
import { gsap } from "gsap";
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const canHover = window.matchMedia("(hover: hover)").matches;
const PULL = 0.4; // ボタンが寄る割合(領域の中心からの距離に掛ける)
const LABEL_PULL = 0.15; // 文字がボタンの中でさらに寄る割合。ボタンより先へ行くので奥行きが出る
document.querySelectorAll(".magnetic").forEach((area) => {
const button = area.querySelector(".magnetic__button");
const label = area.querySelector(".magnetic__label");
// 寄せる相手のいないタッチ端末と、動きを減らす設定では、つながない
if (!canHover || reduceMotion) return;
const release = () => {
gsap.to([button, label], { x: 0, y: 0, duration: 1.1, ease: "elastic.out(1, 0.35)", overwrite: "auto" });
};
const follow = (event) => {
const rect = area.getBoundingClientRect();
const dx = event.clientX - (rect.left + rect.width / 2);
const dy = event.clientY - (rect.top + rect.height / 2);
// overwrite: "auto" が無いと、動かすたびにトゥイーンが積み重なって、追従が重くなる
gsap.to(button, { x: dx * PULL, y: dy * PULL, duration: 0.5, ease: "power3.out", overwrite: "auto" });
gsap.to(label, { x: dx * LABEL_PULL, y: dy * LABEL_PULL, duration: 0.5, ease: "power3.out", overwrite: "auto" });
};
area.addEventListener("pointermove", follow);
area.addEventListener("pointerleave", release);
// キーボードで来たときは中央へ戻す(寄ったままだと、フォーカスの輪がボタンからずれて見える)
button.addEventListener("focus", release);
button.addEventListener("blur", release);
});