マグネティックカーソル
一定半径内でボタンをカーソル方向へ引き寄せ、ラベルに弾力を持たせる磁石風インタラクション。CTAボタンの注目度を高めます。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk: SaaS料金プラン1画面。磁石カーソルでCTAを引き寄せるのが主役 -->
<div class="fd" data-magnetic-root>
<header class="fd__bar">
<span class="fd__logo"><span class="fd__mark"></span>FlowDesk</span>
<nav class="fd__nav">
<a href="#">機能</a>
<a href="#">料金</a>
<a href="#">導入事例</a>
</nav>
</header>
<section class="fd__main">
<p class="fd__kicker">PRICING</p>
<h1 class="fd__title">チームに合わせて選べる2プラン</h1>
<div class="fd__plans">
<div class="fd__card">
<p class="fd__plan">Starter</p>
<p class="fd__amount">¥0<span>/月</span></p>
<p class="fd__note">5名まで・基本機能</p>
<button class="magnet" data-magnet type="button">
<span class="magnet-label">無料で始める</span>
</button>
</div>
<div class="fd__card fd__card--hot">
<span class="fd__badge">人気</span>
<p class="fd__plan">Business</p>
<p class="fd__amount">¥1,200<span>/月</span></p>
<p class="fd__note">無制限・高度な分析</p>
<button class="magnet magnet--primary" data-magnet type="button">
<span class="magnet-label">今すぐ試す →</span>
</button>
</div>
</div>
<p class="fd__hint">ボタンにカーソルを近づけると吸い寄せられます</p>
</section>
<!-- 主役: 磁石カーソルのドット -->
<div class="magnet-cursor" data-cursor></div>
</div>
CSS
/* FlowDesk SaaSテーマ: 紺/青/白 */
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
background:
radial-gradient(circle at 80% 14%, rgba(79,124,255,.3) 0%, transparent 44%),
radial-gradient(circle at 10% 90%, rgba(79,124,255,.16) 0%, transparent 48%),
#0f1b34;
color: #fff;
overflow: hidden;
cursor: none;
}
.fd {
position: relative;
height: 400px;
display: flex;
flex-direction: column;
padding: 0 30px;
}
/* ヘッダー */
.fd__bar {
display: flex;
align-items: center;
gap: 24px;
padding: 15px 0;
font-size: 14px;
}
.fd__logo {
display: inline-flex;
align-items: center;
gap: 9px;
font-weight: 800;
font-size: 16px;
}
.fd__mark {
width: 18px;
height: 18px;
border-radius: 6px;
background: linear-gradient(135deg, #4f7cff, #8fb0ff);
box-shadow: 0 0 14px rgba(79,124,255,.6);
}
.fd__nav {
display: flex;
gap: 20px;
margin-left: auto;
}
.fd__nav a { color: rgba(255,255,255,.72); text-decoration: none; }
/* メイン */
.fd__main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 6px;
}
.fd__kicker {
margin: 0;
font-size: 12px;
font-weight: 700;
letter-spacing: .22em;
color: #8fb0ff;
}
.fd__title {
margin: 0 0 16px;
font-size: 23px;
font-weight: 800;
}
/* プランカード */
.fd__plans {
display: flex;
gap: 16px;
}
.fd__card {
position: relative;
width: 170px;
padding: 18px 16px;
text-align: center;
background: rgba(255,255,255,.05);
border: 1px solid rgba(255,255,255,.12);
border-radius: 16px;
}
.fd__card--hot {
background: rgba(79,124,255,.12);
border-color: rgba(79,124,255,.5);
box-shadow: 0 16px 40px rgba(79,124,255,.3);
}
.fd__badge {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
font-weight: 700;
color: #0f1b34;
background: #8fb0ff;
padding: 3px 12px;
border-radius: 999px;
}
.fd__plan {
margin: 0 0 6px;
font-size: 13px;
font-weight: 700;
color: rgba(255,255,255,.8);
}
.fd__amount {
margin: 0 0 4px;
font-size: 26px;
font-weight: 800;
}
.fd__amount span { font-size: 12px; font-weight: 600; color: rgba(255,255,255,.55); }
.fd__note {
margin: 0 0 14px;
font-size: 11px;
color: rgba(255,255,255,.5);
}
/* 主役: 磁石ボタン */
.magnet {
display: inline-block;
padding: 10px 18px;
font-size: 13px;
font-weight: 700;
color: #fff;
background: rgba(255,255,255,.1);
border: 1px solid rgba(255,255,255,.24);
border-radius: 999px;
cursor: none;
transition: transform .35s cubic-bezier(.22,1,.36,1),
background .25s ease;
}
.magnet--primary {
background: linear-gradient(135deg, #4f7cff, #6f96ff);
border-color: transparent;
box-shadow: 0 12px 28px rgba(79,124,255,.45);
}
.magnet-label {
display: inline-block;
transition: transform .35s cubic-bezier(.22,1,.36,1);
}
.fd__hint {
margin: 14px 0 0;
font-size: 12px;
letter-spacing: .04em;
color: rgba(255,255,255,.42);
}
/* 磁石カーソルのドット */
.magnet-cursor {
position: fixed;
top: 0;
left: 0;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border-radius: 50%;
border: 2px solid rgba(143,176,255,.9);
background: rgba(79,124,255,.18);
pointer-events: none;
z-index: 50;
opacity: 0;
transition: opacity .3s ease, width .25s ease, height .25s ease,
margin .25s ease, background .25s ease;
}
.fd.is-active .magnet-cursor { opacity: 1; }
.magnet-cursor.is-near {
width: 30px;
height: 30px;
margin: -15px 0 0 -15px;
background: rgba(79,124,255,.32);
}
@media (prefers-reduced-motion: reduce) {
.magnet, .magnet-label { transition: background .25s ease; }
}
JavaScript
// FlowDesk: 磁石カーソル。待機中は仮想カーソルがCTAへ自動で吸い寄り、操作で本物に追従
(() => {
const root = document.querySelector('[data-magnetic-root]');
const cursor = document.querySelector('[data-cursor]');
const magnets = Array.from(document.querySelectorAll('[data-magnet]'));
if (!root || !cursor || magnets.length === 0) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const RADIUS = 130; // 磁力が効く半径(px)
const PULL = 0.4; // ボタン本体の引き寄せ強度
const LABEL_PULL = 0.7; // ラベルの引き寄せ強度
let px = 0, py = 0; // 現在のカーソル座標(本物/仮想)
let usePointer = false;
let lastMove = 0;
const IDLE = 1400; // 無操作で自動巡回へ(ms)
root.classList.add('is-active');
// 指定座標で磁力を適用し、カーソルの近接状態を返す
const applyMagnet = (x, y) => {
let near = false;
magnets.forEach((btn) => {
const r = btn.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = x - cx;
const dy = y - cy;
const dist = Math.hypot(dx, dy);
const label = btn.querySelector('.magnet-label');
if (!reduce && dist < RADIUS) {
const force = 1 - dist / RADIUS; // 近いほど強く
btn.style.transform = `translate(${dx * PULL * force}px, ${dy * PULL * force}px)`;
if (label) label.style.transform =
`translate(${dx * LABEL_PULL * force}px, ${dy * LABEL_PULL * force}px)`;
near = true;
} else {
btn.style.transform = 'translate(0,0)';
if (label) label.style.transform = 'translate(0,0)';
}
});
cursor.classList.toggle('is-near', near);
};
root.addEventListener('pointermove', (e) => {
usePointer = true;
lastMove = performance.now();
px = e.clientX; py = e.clientY;
});
root.addEventListener('pointerleave', () => { usePointer = false; });
// 仮想カーソルの自動経路: 2つのボタン中心を行き来して吸い寄りを見せる
const autoPos = (t) => {
const a = magnets[0].getBoundingClientRect();
const b = magnets[magnets.length - 1].getBoundingClientRect();
const ax = a.left + a.width / 2, ay = a.top + a.height / 2;
const bx = b.left + b.width / 2, by = b.top + b.height / 2;
const k = (Math.sin(t * 0.0009) + 1) / 2; // 0..1を往復
return { x: ax + (bx - ax) * k, y: ay + (by - ay) * k };
};
const loop = (now) => {
if (usePointer && now - lastMove > IDLE) usePointer = false;
if (!usePointer) {
const p = autoPos(now);
px = p.x; py = p.y;
}
cursor.style.transform = `translate(${px}px, ${py}px)`;
applyMagnet(px, py);
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
})();
コード
HTML
<!-- マグネティックカーソル:カーソルが近づくとボタンが磁石のように引き寄せられる -->
<div class="stage" data-magnetic-root>
<div class="content">
<h1 class="title">マグネティック</h1>
<p class="lead">ボタンに近づくと、磁石のように吸い寄せられます。</p>
<div class="btn-row">
<button class="magnet" data-magnet>
<span class="magnet-label">Hover me</span>
</button>
<button class="magnet ghost" data-magnet>
<span class="magnet-label">Pull me</span>
</button>
</div>
</div>
<!-- 追従ドット -->
<div class="cursor" data-cursor></div>
</div>
CSS
* { 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(700px 380px at 50% 0%, #14324a 0%, transparent 60%),
linear-gradient(160deg, #0a0e1a, #0e1322);
color: #eaf2ff;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
cursor: none;
}
.content { text-align: center; padding: 24px; z-index: 1; }
.title {
margin: 0 0 10px;
font-size: clamp(32px, 6.5vw, 52px);
font-weight: 800;
letter-spacing: .04em;
color: #fff;
}
.lead { margin: 0 0 26px; color: #9fb3d4; font-size: 14px; }
.btn-row { display: flex; gap: 22px; justify-content: center; flex-wrap: wrap; }
/* 磁石ボタン:JSでtransformを上書きするので transition は戻り用 */
.magnet {
position: relative;
padding: 16px 30px;
border: none;
border-radius: 16px;
font-size: 15px;
font-weight: 700;
color: #061018;
background: linear-gradient(135deg, #5ce1ff, #46a8ff);
box-shadow: 0 12px 30px rgba(70,168,255,.35);
transition: transform .35s cubic-bezier(.2,.8,.2,1), box-shadow .35s ease;
will-change: transform;
}
.magnet.ghost {
color: #cfe3ff;
background: transparent;
border: 1.5px solid rgba(92,225,255,.55);
box-shadow: none;
}
.magnet:hover { box-shadow: 0 18px 40px rgba(70,168,255,.5); }
/* ラベルはボタンより少し弱く引っ張ると“弾力”が出る */
.magnet-label {
display: inline-block;
transition: transform .35s cubic-bezier(.2,.8,.2,1);
will-change: transform;
pointer-events: none;
}
/* カーソルドット */
.cursor {
position: fixed;
top: 0; left: 0;
width: 14px; height: 14px;
border-radius: 50%;
background: #5ce1ff;
box-shadow: 0 0 16px rgba(92,225,255,.9);
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9999;
opacity: 0; /* 初回移動まで非表示(中央の文字に重ならない) */
transition: width .2s ease, height .2s ease, background .2s ease, opacity .3s ease;
}
[data-magnetic-root].is-active .cursor { opacity: 1; }
.cursor.is-near { width: 26px; height: 26px; background: #fff; }
@media (prefers-reduced-motion: reduce) {
.magnet, .magnet-label { transition: none; }
}
JavaScript
// マグネティックカーソル:一定距離内でボタンをカーソル方向へ引き寄せる
(() => {
const root = document.querySelector('[data-magnetic-root]');
const cursor = document.querySelector('[data-cursor]');
const magnets = Array.from(document.querySelectorAll('[data-magnet]'));
if (!root || !cursor || magnets.length === 0) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const RADIUS = 120; // 磁力が効く半径(px)
const PULL = 0.4; // ボタン本体の引き寄せ強度
const LABEL_PULL = 0.7; // ラベルの引き寄せ強度(弾力演出)
let near = false;
root.addEventListener('pointermove', (e) => {
// カーソルドットを即時追従
cursor.style.transform = `translate(${e.clientX}px, ${e.clientY}px) translate(-50%, -50%)`;
if (!root.classList.contains('is-active')) root.classList.add('is-active');
near = false;
magnets.forEach((btn) => {
const r = btn.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = e.clientX - cx;
const dy = e.clientY - cy;
const dist = Math.hypot(dx, dy);
const label = btn.querySelector('.magnet-label');
if (!reduce && dist < RADIUS) {
// 距離に応じて引き寄せ(近いほど反応を緩めて自然に)
const force = 1 - dist / RADIUS;
btn.style.transform = `translate(${dx * PULL * force}px, ${dy * PULL * force}px)`;
if (label) label.style.transform = `translate(${dx * LABEL_PULL * force}px, ${dy * LABEL_PULL * force}px)`;
near = true;
} else {
// 範囲外は元に戻す
btn.style.transform = 'translate(0, 0)';
if (label) label.style.transform = 'translate(0, 0)';
}
});
cursor.classList.toggle('is-near', near);
});
// 領域を離れたら全リセット
root.addEventListener('pointerleave', () => {
magnets.forEach((btn) => {
btn.style.transform = 'translate(0, 0)';
const label = btn.querySelector('.magnet-label');
if (label) label.style.transform = 'translate(0, 0)';
});
cursor.classList.remove('is-near');
root.classList.remove('is-active');
});
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「マグネティックカーソル」の効果を追加してください。
# 追加してほしい効果
マグネティックカーソル(カスタムカーソル)
一定半径内でボタンをカーソル方向へ引き寄せ、ラベルに弾力を持たせる磁石風インタラクション。CTAボタンの注目度を高めます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- マグネティックカーソル:カーソルが近づくとボタンが磁石のように引き寄せられる -->
<div class="stage" data-magnetic-root>
<div class="content">
<h1 class="title">マグネティック</h1>
<p class="lead">ボタンに近づくと、磁石のように吸い寄せられます。</p>
<div class="btn-row">
<button class="magnet" data-magnet>
<span class="magnet-label">Hover me</span>
</button>
<button class="magnet ghost" data-magnet>
<span class="magnet-label">Pull me</span>
</button>
</div>
</div>
<!-- 追従ドット -->
<div class="cursor" data-cursor></div>
</div>
【CSS】
* { 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(700px 380px at 50% 0%, #14324a 0%, transparent 60%),
linear-gradient(160deg, #0a0e1a, #0e1322);
color: #eaf2ff;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
cursor: none;
}
.content { text-align: center; padding: 24px; z-index: 1; }
.title {
margin: 0 0 10px;
font-size: clamp(32px, 6.5vw, 52px);
font-weight: 800;
letter-spacing: .04em;
color: #fff;
}
.lead { margin: 0 0 26px; color: #9fb3d4; font-size: 14px; }
.btn-row { display: flex; gap: 22px; justify-content: center; flex-wrap: wrap; }
/* 磁石ボタン:JSでtransformを上書きするので transition は戻り用 */
.magnet {
position: relative;
padding: 16px 30px;
border: none;
border-radius: 16px;
font-size: 15px;
font-weight: 700;
color: #061018;
background: linear-gradient(135deg, #5ce1ff, #46a8ff);
box-shadow: 0 12px 30px rgba(70,168,255,.35);
transition: transform .35s cubic-bezier(.2,.8,.2,1), box-shadow .35s ease;
will-change: transform;
}
.magnet.ghost {
color: #cfe3ff;
background: transparent;
border: 1.5px solid rgba(92,225,255,.55);
box-shadow: none;
}
.magnet:hover { box-shadow: 0 18px 40px rgba(70,168,255,.5); }
/* ラベルはボタンより少し弱く引っ張ると“弾力”が出る */
.magnet-label {
display: inline-block;
transition: transform .35s cubic-bezier(.2,.8,.2,1);
will-change: transform;
pointer-events: none;
}
/* カーソルドット */
.cursor {
position: fixed;
top: 0; left: 0;
width: 14px; height: 14px;
border-radius: 50%;
background: #5ce1ff;
box-shadow: 0 0 16px rgba(92,225,255,.9);
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9999;
opacity: 0; /* 初回移動まで非表示(中央の文字に重ならない) */
transition: width .2s ease, height .2s ease, background .2s ease, opacity .3s ease;
}
[data-magnetic-root].is-active .cursor { opacity: 1; }
.cursor.is-near { width: 26px; height: 26px; background: #fff; }
@media (prefers-reduced-motion: reduce) {
.magnet, .magnet-label { transition: none; }
}
【JavaScript】
// マグネティックカーソル:一定距離内でボタンをカーソル方向へ引き寄せる
(() => {
const root = document.querySelector('[data-magnetic-root]');
const cursor = document.querySelector('[data-cursor]');
const magnets = Array.from(document.querySelectorAll('[data-magnet]'));
if (!root || !cursor || magnets.length === 0) return; // null安全
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const RADIUS = 120; // 磁力が効く半径(px)
const PULL = 0.4; // ボタン本体の引き寄せ強度
const LABEL_PULL = 0.7; // ラベルの引き寄せ強度(弾力演出)
let near = false;
root.addEventListener('pointermove', (e) => {
// カーソルドットを即時追従
cursor.style.transform = `translate(${e.clientX}px, ${e.clientY}px) translate(-50%, -50%)`;
if (!root.classList.contains('is-active')) root.classList.add('is-active');
near = false;
magnets.forEach((btn) => {
const r = btn.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = e.clientX - cx;
const dy = e.clientY - cy;
const dist = Math.hypot(dx, dy);
const label = btn.querySelector('.magnet-label');
if (!reduce && dist < RADIUS) {
// 距離に応じて引き寄せ(近いほど反応を緩めて自然に)
const force = 1 - dist / RADIUS;
btn.style.transform = `translate(${dx * PULL * force}px, ${dy * PULL * force}px)`;
if (label) label.style.transform = `translate(${dx * LABEL_PULL * force}px, ${dy * LABEL_PULL * force}px)`;
near = true;
} else {
// 範囲外は元に戻す
btn.style.transform = 'translate(0, 0)';
if (label) label.style.transform = 'translate(0, 0)';
}
});
cursor.classList.toggle('is-near', near);
});
// 領域を離れたら全リセット
root.addEventListener('pointerleave', () => {
magnets.forEach((btn) => {
btn.style.transform = 'translate(0, 0)';
const label = btn.querySelector('.magnet-label');
if (label) label.style.transform = 'translate(0, 0)';
});
cursor.classList.remove('is-near');
root.classList.remove('is-active');
});
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。