デモ
デモを読み込めませんでした。
仕様
- 用途
- カーソル
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
独自カーソルは、遅れずに付いてくる小さな点と、少し遅れて追いかける大きな輪の 2 つで作ります。どちらも画面に固定して置き、pointer-events を none にして、下のリンクやボタンを押せるようにします。ポインタが動くたびに、点は位置をそのまま書き込み、輪は quickTo でその位置へ向かわせます。
輪が追いつく時間は 0.45 秒にしています。0.3 秒ではほとんど点と重なって遅れが見えず、0.6 秒を超えると輪がふわふわ漂って、どこを指しているのか読み取りにくくなります。終わりのゆるやかな power3 と組み合わせると、止まった瞬間に吸い付くように収まります。
本物のカーソルは cursor: none で消しますが、消すのは独自カーソルを実際に出したときだけにします。指で触る端末や、スクリプトが動かなかったときまで消すと、画面に何も指すものが無くなります。中心をポインタに合わせる指定も忘れやすく、抜けると点と輪が右下へずれて重なります。
つまみは、輪の直径と追いつく時間の 2 つです。輪を大きく遅くするとゆったりした作品集の雰囲気になり、小さく速くすると操作の手応えが素直になります。リンクの上で輪だけを膨らませる、色を変える、といった足し方もよく合います。ポートフォリオやスタジオのサイト、キャンペーンページに向いています。
コード
<!-- 独自カーソル。飾りなので読み上げからは外し、<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: #d4ff3a; /* 点 */
}
.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");
// 動きを減らす設定では、輪も遅れずに付いてくる
const duration = reduceMotion ? 0 : 0.45;
const ringX = gsap.quickTo(ring, "x", { duration, ease: "power3" });
const ringY = gsap.quickTo(ring, "y", { duration, ease: "power3" });
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 });
});
}