追従カスタムカーソル
即時追従のドットと遅延追従のリングを線形補間(lerp)で重ねた2層カーソル。サイトのブランド演出やインタラクティブなナビに使えます。
ライブデモ
使用例(お題: カフェ MOON BREW)
この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- MOON BREW: カフェのメニュー1画面。追従カーソル(ドット+遅延リング)を主役に -->
<div class="mb" data-cursor-root>
<header class="mb__bar">
<span class="mb__logo"><span class="mb__cup">☕</span>MOON BREW</span>
<nav class="mb__nav">
<a href="#" data-hover>メニュー</a>
<a href="#" data-hover>店舗</a>
<a href="#" data-hover>豆を買う</a>
</nav>
</header>
<section class="mb__body">
<div class="mb__intro">
<p class="mb__kicker">TODAY'S BREW</p>
<h1 class="mb__title">月夜に、<br>とっておきの一杯を。</h1>
<p class="mb__lead">深煎りの香りとなめらかな泡。<br>静かな夜にそっと寄り添うコーヒー。</p>
<span class="mb__order" data-hover>メニューを見る</span>
</div>
<ul class="mb__menu">
<li class="mb__item" data-hover>
<span class="mb__name">カフェラテ</span>
<span class="mb__price">¥520</span>
</li>
<li class="mb__item" data-hover>
<span class="mb__name">月見モカ</span>
<span class="mb__price">¥580</span>
</li>
<li class="mb__item" data-hover>
<span class="mb__name">琥珀ハニー</span>
<span class="mb__price">¥560</span>
</li>
</ul>
</section>
<!-- 主役: 2層カーソル -->
<div class="cursor-dot" data-dot></div>
<div class="cursor-ring" data-ring></div>
</div>
CSS
/* MOON BREW カフェテーマ: クリーム/濃ブラウン/琥珀 */
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Hiragino Sans", "Yu Gothic", system-ui, sans-serif;
background:
radial-gradient(circle at 82% 16%, rgba(201,138,59,.22) 0%, transparent 44%),
radial-gradient(circle at 8% 92%, rgba(43,29,18,.18) 0%, transparent 46%),
#f5ede1;
color: #2b1d12;
overflow: hidden;
cursor: none; /* 既定カーソルを隠して自作を主役に */
}
.mb {
position: relative;
height: 400px;
display: flex;
flex-direction: column;
padding: 0 28px;
}
/* ヘッダー */
.mb__bar {
display: flex;
align-items: center;
gap: 24px;
padding: 16px 0;
font-size: 14px;
}
.mb__logo {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 800;
letter-spacing: .04em;
font-size: 16px;
}
.mb__cup { font-size: 18px; }
.mb__nav {
display: flex;
gap: 20px;
margin-left: auto;
}
.mb__nav a {
color: #5a4632;
text-decoration: none;
position: relative;
}
/* 本文: 左コピー + 右メニュー */
.mb__body {
flex: 1;
display: grid;
grid-template-columns: 1.15fr .85fr;
gap: 26px;
align-items: center;
}
.mb__kicker {
margin: 0 0 8px;
font-size: 12px;
font-weight: 700;
letter-spacing: .22em;
color: #c98a3b;
}
.mb__title {
margin: 0 0 12px;
font-size: 30px;
line-height: 1.32;
font-weight: 800;
}
.mb__lead {
margin: 0 0 18px;
font-size: 13px;
line-height: 1.8;
color: #6b5640;
}
.mb__order {
display: inline-block;
padding: 11px 22px;
font-size: 13px;
font-weight: 700;
color: #f5ede1;
background: linear-gradient(135deg, #2b1d12, #4a3320);
border-radius: 999px;
box-shadow: 0 10px 22px rgba(43,29,18,.28);
}
/* メニューカード */
.mb__menu {
list-style: none;
margin: 0;
padding: 14px;
display: flex;
flex-direction: column;
gap: 8px;
background: rgba(255,255,255,.66);
border: 1px solid rgba(201,138,59,.28);
border-radius: 16px;
box-shadow: 0 14px 34px rgba(43,29,18,.12);
}
.mb__item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 11px 14px;
border-radius: 11px;
font-size: 14px;
font-weight: 600;
background: rgba(245,237,225,.6);
transition: background .25s ease;
}
.mb__price { color: #c98a3b; font-weight: 700; }
/* 主役: 追従カーソル(ドット+リング) */
.cursor-dot,
.cursor-ring {
position: fixed;
top: 0;
left: 0;
pointer-events: none;
z-index: 50;
opacity: 0;
transition: opacity .3s ease;
}
.mb.is-active .cursor-dot,
.mb.is-active .cursor-ring { opacity: 1; }
.cursor-dot {
width: 8px;
height: 8px;
margin: -4px 0 0 -4px;
border-radius: 50%;
background: #2b1d12;
}
.cursor-ring {
width: 34px;
height: 34px;
margin: -17px 0 0 -17px;
border-radius: 50%;
border: 2px solid #c98a3b;
transition: opacity .3s ease, width .3s ease, height .3s ease,
margin .3s ease, background .3s ease;
}
.cursor-ring.is-hover {
width: 56px;
height: 56px;
margin: -28px 0 0 -28px;
background: rgba(201,138,59,.16);
}
@media (prefers-reduced-motion: reduce) {
.cursor-dot, .cursor-ring { transition: opacity .2s ease; }
}
JavaScript
// MOON BREW: ドット即時+リング遅延の2層カーソル。待機中は自動巡回、操作で本物に追従
(() => {
const root = document.querySelector('[data-cursor-root]');
const dot = document.querySelector('[data-dot]');
const ring = document.querySelector('[data-ring]');
if (!root || !dot || !ring) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 目標(マウス/仮想)とリングの現在位置
let targetX = window.innerWidth / 2;
let targetY = window.innerHeight / 2;
let ringX = targetX, ringY = targetY;
let usePointer = false; // 本物のポインタ追従中か
let lastMove = 0; // 最後にポインタが動いた時刻
const IDLE = 1500; // 無操作でこの時間が過ぎたら自動巡回へ戻る(ms)
// 起動直後からカーソルを見せる(一覧プレビュー対策)
root.classList.add('is-active');
// マウス移動で目標更新し、ドットは即追従
window.addEventListener('pointermove', (e) => {
usePointer = true;
lastMove = performance.now();
targetX = e.clientX;
targetY = e.clientY;
root.classList.add('is-active');
});
window.addEventListener('pointerleave', () => { usePointer = false; });
// ホバー対象でリング拡大
document.querySelectorAll('[data-hover]').forEach((el) => {
el.addEventListener('pointerenter', () => ring.classList.add('is-hover'));
el.addEventListener('pointerleave', () => ring.classList.remove('is-hover'));
});
// 仮想カーソルの自動経路(リサージュ): メニュー周辺をゆっくり巡回
const autoPos = (t) => {
const r = root.getBoundingClientRect();
const cx = r.left + r.width * 0.62;
const cy = r.top + r.height * 0.52;
return {
x: cx + Math.sin(t * 0.00075) * r.width * 0.22,
y: cy + Math.sin(t * 0.0011 + 0.8) * r.height * 0.2,
};
};
const ease = reduce ? 1 : 0.18;
const tick = (now) => {
// 一定時間操作が無ければ自動巡回へ戻す
if (usePointer && now - lastMove > IDLE) usePointer = false;
if (!usePointer) {
const p = autoPos(now);
targetX = p.x;
targetY = p.y;
}
// ドットは即時、リングは補間で遅延追従
dot.style.transform = `translate(${targetX}px, ${targetY}px)`;
ringX += (targetX - ringX) * ease;
ringY += (targetY - ringY) * ease;
ring.style.transform = `translate(${ringX}px, ${ringY}px)`;
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
})();
コード
HTML
<!-- 追従カスタムカーソル:リング(遅延追従)とドット(即時)の2層構成 -->
<div class="stage" data-cursor-root>
<div class="content">
<p class="kicker">CUSTOM CURSOR</p>
<h1 class="title">追従カーソル</h1>
<p class="lead">マウスを動かすと、リングが少し遅れてやさしく追いかけます。</p>
<div class="chips">
<span class="chip" data-hover>Design</span>
<span class="chip" data-hover>Motion</span>
<span class="chip" data-hover>Interaction</span>
</div>
</div>
<!-- カーソル本体(2層) -->
<div class="cursor-dot" data-dot></div>
<div class="cursor-ring" data-ring></div>
</div>
CSS
/* ベース:暗めの上品な背景。iframe内なので body から自由に */
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
position: relative;
height: 360px;
display: grid;
place-items: center;
overflow: hidden;
background:
radial-gradient(1200px 400px at 70% -10%, #2b2256 0%, transparent 60%),
radial-gradient(900px 500px at 0% 120%, #163a4f 0%, transparent 55%),
#0d0f1a;
color: #eef0ff;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
cursor: none; /* OS既定カーソルを隠す */
}
.content { text-align: center; padding: 24px; z-index: 1; }
.kicker {
margin: 0 0 10px;
letter-spacing: .42em;
font-size: 11px;
color: #8ea2ff;
font-weight: 700;
}
.title {
margin: 0 0 12px;
font-size: clamp(34px, 7vw, 56px);
font-weight: 800;
letter-spacing: .02em;
background: linear-gradient(90deg, #fff, #b8c2ff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lead { margin: 0 0 22px; color: #b9bedd; font-size: 14px; }
.chips { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.chip {
padding: 9px 18px;
border-radius: 999px;
border: 1px solid rgba(142,162,255,.35);
background: rgba(255,255,255,.04);
color: #dfe3ff;
font-size: 13px;
font-weight: 600;
transition: background .25s ease, transform .25s ease, border-color .25s ease;
}
.chip:hover {
background: rgba(142,162,255,.18);
border-color: rgba(142,162,255,.7);
transform: translateY(-2px);
}
/* カーソル:ドット(即時)とリング(遅延追従) */
.cursor-dot, .cursor-ring {
position: fixed;
top: 0; left: 0;
border-radius: 50%;
pointer-events: none; /* クリックを邪魔しない */
z-index: 9999;
transform: translate(-50%, -50%);
will-change: transform;
/* 初回ポインタ移動まで非表示(中央の文字に重ならないように) */
opacity: 0;
transition: opacity .3s ease;
}
[data-cursor-root].is-active .cursor-dot,
[data-cursor-root].is-active .cursor-ring { opacity: 1; }
/* リングだけは個別 transition も維持 */
.cursor-ring {
transition: width .25s ease, height .25s ease,
border-color .25s ease, background .25s ease, opacity .3s ease;
}
.cursor-dot {
width: 8px; height: 8px;
background: #8ea2ff;
box-shadow: 0 0 12px rgba(142,162,255,.9);
}
.cursor-ring {
width: 38px; height: 38px;
border: 2px solid rgba(142,162,255,.75);
}
/* ホバー時にリングを拡大して反転的に強調 */
.cursor-ring.is-hover {
width: 64px; height: 64px;
background: rgba(142,162,255,.12);
border-color: #b8c2ff;
}
/* モーション控えめ設定:追従の遅延を切る */
@media (prefers-reduced-motion: reduce) {
.chip { transition: none; }
}
JavaScript
// 追従カスタムカーソル:ドットは即時、リングは線形補間(lerp)で遅延追従
(() => {
const root = document.querySelector('[data-cursor-root]');
const dot = document.querySelector('[data-dot]');
const ring = document.querySelector('[data-ring]');
if (!root || !dot || !ring) return; // null安全
// モーション控えめなら追従遅延を無効化
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 目標座標(マウス位置)と、リングの現在座標
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
let ringX = mouseX;
let ringY = mouseY;
// マウス移動で目標を更新し、ドットは即追従
window.addEventListener('pointermove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
dot.style.transform = `translate(${mouseX}px, ${mouseY}px) translate(-50%, -50%)`;
// 初回移動でカーソルを表示(中央の文字との重なりを避ける)
if (!root.classList.contains('is-active')) root.classList.add('is-active');
});
// 領域外に出たらカーソルを隠す
window.addEventListener('pointerleave', () => root.classList.remove('is-active'));
// ホバー対象でリングを拡大
document.querySelectorAll('[data-hover]').forEach((el) => {
el.addEventListener('pointerenter', () => ring.classList.add('is-hover'));
el.addEventListener('pointerleave', () => ring.classList.remove('is-hover'));
});
// 描画ループ:リングを少しずつ目標へ近づける
const ease = reduce ? 1 : 0.18;
const tick = () => {
ringX += (mouseX - ringX) * ease;
ringY += (mouseY - ringY) * ease;
ring.style.transform = `translate(${ringX}px, ${ringY}px) translate(-50%, -50%)`;
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「追従カスタムカーソル」の効果を追加してください。
# 追加してほしい効果
追従カスタムカーソル(カスタムカーソル)
即時追従のドットと遅延追従のリングを線形補間(lerp)で重ねた2層カーソル。サイトのブランド演出やインタラクティブなナビに使えます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 追従カスタムカーソル:リング(遅延追従)とドット(即時)の2層構成 -->
<div class="stage" data-cursor-root>
<div class="content">
<p class="kicker">CUSTOM CURSOR</p>
<h1 class="title">追従カーソル</h1>
<p class="lead">マウスを動かすと、リングが少し遅れてやさしく追いかけます。</p>
<div class="chips">
<span class="chip" data-hover>Design</span>
<span class="chip" data-hover>Motion</span>
<span class="chip" data-hover>Interaction</span>
</div>
</div>
<!-- カーソル本体(2層) -->
<div class="cursor-dot" data-dot></div>
<div class="cursor-ring" data-ring></div>
</div>
【CSS】
/* ベース:暗めの上品な背景。iframe内なので body から自由に */
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
position: relative;
height: 360px;
display: grid;
place-items: center;
overflow: hidden;
background:
radial-gradient(1200px 400px at 70% -10%, #2b2256 0%, transparent 60%),
radial-gradient(900px 500px at 0% 120%, #163a4f 0%, transparent 55%),
#0d0f1a;
color: #eef0ff;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
cursor: none; /* OS既定カーソルを隠す */
}
.content { text-align: center; padding: 24px; z-index: 1; }
.kicker {
margin: 0 0 10px;
letter-spacing: .42em;
font-size: 11px;
color: #8ea2ff;
font-weight: 700;
}
.title {
margin: 0 0 12px;
font-size: clamp(34px, 7vw, 56px);
font-weight: 800;
letter-spacing: .02em;
background: linear-gradient(90deg, #fff, #b8c2ff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lead { margin: 0 0 22px; color: #b9bedd; font-size: 14px; }
.chips { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.chip {
padding: 9px 18px;
border-radius: 999px;
border: 1px solid rgba(142,162,255,.35);
background: rgba(255,255,255,.04);
color: #dfe3ff;
font-size: 13px;
font-weight: 600;
transition: background .25s ease, transform .25s ease, border-color .25s ease;
}
.chip:hover {
background: rgba(142,162,255,.18);
border-color: rgba(142,162,255,.7);
transform: translateY(-2px);
}
/* カーソル:ドット(即時)とリング(遅延追従) */
.cursor-dot, .cursor-ring {
position: fixed;
top: 0; left: 0;
border-radius: 50%;
pointer-events: none; /* クリックを邪魔しない */
z-index: 9999;
transform: translate(-50%, -50%);
will-change: transform;
/* 初回ポインタ移動まで非表示(中央の文字に重ならないように) */
opacity: 0;
transition: opacity .3s ease;
}
[data-cursor-root].is-active .cursor-dot,
[data-cursor-root].is-active .cursor-ring { opacity: 1; }
/* リングだけは個別 transition も維持 */
.cursor-ring {
transition: width .25s ease, height .25s ease,
border-color .25s ease, background .25s ease, opacity .3s ease;
}
.cursor-dot {
width: 8px; height: 8px;
background: #8ea2ff;
box-shadow: 0 0 12px rgba(142,162,255,.9);
}
.cursor-ring {
width: 38px; height: 38px;
border: 2px solid rgba(142,162,255,.75);
}
/* ホバー時にリングを拡大して反転的に強調 */
.cursor-ring.is-hover {
width: 64px; height: 64px;
background: rgba(142,162,255,.12);
border-color: #b8c2ff;
}
/* モーション控えめ設定:追従の遅延を切る */
@media (prefers-reduced-motion: reduce) {
.chip { transition: none; }
}
【JavaScript】
// 追従カスタムカーソル:ドットは即時、リングは線形補間(lerp)で遅延追従
(() => {
const root = document.querySelector('[data-cursor-root]');
const dot = document.querySelector('[data-dot]');
const ring = document.querySelector('[data-ring]');
if (!root || !dot || !ring) return; // null安全
// モーション控えめなら追従遅延を無効化
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 目標座標(マウス位置)と、リングの現在座標
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
let ringX = mouseX;
let ringY = mouseY;
// マウス移動で目標を更新し、ドットは即追従
window.addEventListener('pointermove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
dot.style.transform = `translate(${mouseX}px, ${mouseY}px) translate(-50%, -50%)`;
// 初回移動でカーソルを表示(中央の文字との重なりを避ける)
if (!root.classList.contains('is-active')) root.classList.add('is-active');
});
// 領域外に出たらカーソルを隠す
window.addEventListener('pointerleave', () => root.classList.remove('is-active'));
// ホバー対象でリングを拡大
document.querySelectorAll('[data-hover]').forEach((el) => {
el.addEventListener('pointerenter', () => ring.classList.add('is-hover'));
el.addEventListener('pointerleave', () => ring.classList.remove('is-hover'));
});
// 描画ループ:リングを少しずつ目標へ近づける
const ease = reduce ? 1 : 0.18;
const tick = () => {
ringX += (mouseX - ringX) * ease;
ringY += (mouseY - ringY) * ease;
ring.style.transform = `translate(${ringX}px, ${ringY}px) translate(-50%, -50%)`;
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。