デモ
デモを読み込めませんでした。
仕様
- 用途
- カーソル
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
小さな点はポインタにぴたりと付き、大きな輪だけが0.45秒遅れて追いかけて、止まった瞬間に点のまわりへ吸い付くように収まります。どちらも画面に固定し、pointer-events を none にしてあるので、下のリンクやボタンはそのまま押せます。動くたびに点には位置をそのまま書き込み、輪は quickTo でそこへ向かわせているだけです。
遅れがはっきり見えて、しかも指している場所を見失わない長さとして、0.45秒を選びました。0.3秒では輪がほとんど点と重なって遅れが見えず、0.6秒を超えると輪が漂って、どこを指しているのか読み取りにくくなります。終わりがゆるやかなイージング(power3)と組み合わせているので、止まったときの収まりがなめらかです。
本物のカーソルは cursor: none で隠しますが、隠すのは独自カーソルを実際に表示したときだけにしています。いつでも隠してしまうと、タッチ端末や、スクリプトが動かなかったときに、指し示すものが何もなくなります。忘れやすいのが中心合わせで、要素の中心をポインタに合わせる指定が抜けると、点と輪がそれぞれ右下へずれて表示されます。
輪を大きく遅くすればゆったりした作品集の雰囲気に、小さく速くすれば手応えの素直なカーソルになります。リンクの上で輪だけを膨らませる、色を変えるといった演出も、この上にそのまま足していけます。
コード
<!-- 独自カーソル。飾りなので読み上げからは外し、<body> の終わりに置く -->
<span class="cursor-ring" aria-hidden="true"></span>
<span class="cursor-dot" aria-hidden="true"></span>
:root {
--cursor-dot: 14px;
--cursor-ring: 68px;
--cursor-ink: #eeece6; /* 輪の線 */
--cursor-accent: #c8bf83; /* 点 */
}
.cursor-dot,
.cursor-ring {
position: fixed;
left: 0;
top: 0;
z-index: 9999;
border-radius: 50%;
/* 下のリンクやボタンを押せるようにする */
pointer-events: none;
/* JS が有効にしたときだけ表示する。タッチ端末やスクリプトが動かないときは本物のカーソルのまま */
display: none;
}
.cursor-dot {
width: var(--cursor-dot);
height: var(--cursor-dot);
background: var(--cursor-accent);
}
.cursor-ring {
width: var(--cursor-ring);
height: var(--cursor-ring);
box-sizing: border-box;
border: 2px solid var(--cursor-ink);
}
.has-custom-cursor .cursor-dot,
.has-custom-cursor .cursor-ring {
display: block;
}
/* 本物のカーソルは、独自カーソルを表示したときだけ隠す */
.has-custom-cursor,
.has-custom-cursor a,
.has-custom-cursor button {
cursor: none;
}
import { gsap } from "gsap";
const dot = document.querySelector(".cursor-dot");
const ring = document.querySelector(".cursor-ring");
// タッチ端末にはポインタが無いので、独自カーソルは表示せず本物のカーソルのままにする
const canHover = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (dot && ring && canHover) {
// 中心をポインタに合わせる。輪の大きさを変えても位置がずれない
gsap.set([dot, ring], { xPercent: -50, yPercent: -50 });
document.documentElement.classList.add("has-custom-cursor");
// 動きを減らす設定では、輪も遅れずに付いてくる(quickTo は duration が 0 だと値が動かない)
const follow = (prop) => (reduceMotion ? (value) => gsap.set(ring, { [prop]: value }) : gsap.quickTo(ring, prop, { duration: 0.45, ease: "power3" }));
const ringX = follow("x");
const ringY = follow("y");
window.addEventListener("pointermove", (event) => {
gsap.set(dot, { x: event.clientX, y: event.clientY });
ringX(event.clientX);
ringY(event.clientY);
});
// ページの外へ出たら消す(端に貼り付いたまま残らないように)
document.addEventListener("pointerleave", () => {
gsap.to([dot, ring], { autoAlpha: 0, duration: 0.2 });
});
document.addEventListener("pointerenter", () => {
gsap.to([dot, ring], { autoAlpha: 1, duration: 0.2 });
});
}