ホバー拡大カーソル
要素ホバーで円が拡大し、data-labelの文字をカーソル内に表示。カードギャラリーやリンク集の操作ヒント提示に最適です。
ライブデモ
使用例(お題: カフェ MOON BREW)
この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- MOON BREW: カフェのメニューギャラリー1画面。ホバー拡大ラベルカーソルを主役に -->
<div class="mb" data-scale-root>
<header class="mb__bar">
<span class="mb__logo"><span class="mb__cup">☕</span>MOON BREW</span>
<span class="mb__sub">SEASONAL MENU</span>
</header>
<section class="mb__gallery">
<h1 class="mb__title">秋の季節限定メニュー</h1>
<div class="mb__grid">
<figure class="mb__card" data-hover data-label="詳しく見る">
<img class="mb__img" src="https://picsum.photos/220/160?random=21" alt="">
<figcaption>マロンラテ</figcaption>
</figure>
<figure class="mb__card" data-hover data-label="詳しく見る">
<img class="mb__img" src="https://picsum.photos/220/160?random=22" alt="">
<figcaption>焙煎モカ</figcaption>
</figure>
<figure class="mb__card" data-hover data-label="詳しく見る">
<img class="mb__img" src="https://picsum.photos/220/160?random=23" alt="">
<figcaption>黒糖チャイ</figcaption>
</figure>
</div>
<p class="mb__hint">カードに乗るとカーソルが広がります</p>
</section>
<!-- 主役: ラベル付き拡大カーソル -->
<div class="scale-cursor" data-cursor>
<span class="scale-cursor__label" data-cursor-label></span>
</div>
</div>
CSS
/* MOON BREW カフェテーマ: クリーム/濃ブラウン/琥珀 */
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Hiragino Sans", "Yu Gothic", system-ui, sans-serif;
color: #2b1d12;
overflow: hidden;
}
.mb {
position: relative;
height: 400px;
background:
radial-gradient(circle at 84% 14%, rgba(201,138,59,.2) 0%, transparent 46%),
#f5ede1;
cursor: none;
overflow: hidden;
}
/* ヘッダー */
.mb__bar {
display: flex;
align-items: baseline;
gap: 14px;
padding: 15px 26px;
}
.mb__logo {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 800;
font-size: 16px;
letter-spacing: .04em;
}
.mb__cup { font-size: 18px; }
.mb__sub {
font-size: 11px;
font-weight: 700;
letter-spacing: .22em;
color: #b89a76;
}
/* ギャラリー */
.mb__gallery { padding: 0 26px; }
.mb__title {
margin: 0 0 14px;
font-size: 19px;
font-weight: 800;
}
.mb__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.mb__card {
margin: 0;
background: rgba(255,255,255,.7);
border: 1px solid rgba(201,138,59,.26);
border-radius: 14px;
overflow: hidden;
box-shadow: 0 12px 28px rgba(43,29,18,.1);
transition: transform .3s ease;
}
.mb__card:hover { transform: translateY(-4px); }
.mb__img {
display: block;
width: 100%;
height: 150px;
object-fit: cover;
}
.mb__card figcaption {
padding: 10px 12px;
font-size: 14px;
font-weight: 700;
}
.mb__hint {
margin: 14px 0 0;
font-size: 11px;
letter-spacing: .04em;
color: #b89a76;
}
/* 主役: ホバー拡大ラベルカーソル */
.scale-cursor {
position: fixed;
top: 0;
left: 0;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border-radius: 50%;
background: rgba(201,138,59,.9);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 50;
opacity: 0;
transition: opacity .3s ease, width .28s ease, height .28s ease,
margin .28s ease, background .28s ease;
}
.mb.is-active .scale-cursor { opacity: 1; }
.scale-cursor.is-hover {
width: 84px;
height: 84px;
margin: -42px 0 0 -42px;
background: rgba(43,29,18,.92);
}
.scale-cursor__label {
font-size: 11px;
font-weight: 700;
color: #f5ede1;
opacity: 0;
transform: scale(.6);
transition: opacity .25s ease, transform .25s ease;
white-space: nowrap;
}
.scale-cursor.is-hover .scale-cursor__label {
opacity: 1;
transform: scale(1);
}
JavaScript
// MOON BREW: ホバー拡大ラベルカーソル。待機中はカードを自動で巡回、操作で本物に追従
(() => {
const root = document.querySelector('[data-scale-root]');
const cursor = document.querySelector('[data-cursor]');
const label = document.querySelector('[data-cursor-label]');
if (!root || !cursor || !label) return; // null安全
const cards = Array.from(document.querySelectorAll('[data-hover]'));
let px = 0, py = 0;
let usePointer = false;
let lastMove = 0;
const IDLE = 1600;
root.classList.add('is-active');
// ホバー状態をまとめて切替
const setHover = (el) => {
if (el) {
cursor.classList.add('is-hover');
label.textContent = el.dataset.label || '';
} else {
cursor.classList.remove('is-hover');
label.textContent = '';
}
};
root.addEventListener('pointermove', (e) => {
usePointer = true;
lastMove = performance.now();
px = e.clientX; py = e.clientY;
});
root.addEventListener('pointerleave', () => {
usePointer = false;
});
// 本物のホバー検出
cards.forEach((el) => {
el.addEventListener('pointerenter', () => { if (usePointer) setHover(el); });
el.addEventListener('pointerleave', () => { if (usePointer) setHover(null); });
});
// 自動巡回: カードの中心を順番に渡り歩く
const autoState = () => {
if (cards.length === 0) return null;
const t = performance.now() * 0.00018;
const idx = Math.floor(t % cards.length);
const phase = t % 1; // 0..1: カード上に滞在 or 移動
const el = cards[idx];
const r = el.getBoundingClientRect();
return {
x: r.left + r.width / 2,
y: r.top + r.height / 2,
el,
onCard: phase < 0.7, // 大半の時間はカード上に居てラベルを見せる
};
};
const loop = (now) => {
if (usePointer && now - lastMove > IDLE) { usePointer = false; }
if (!usePointer) {
const s = autoState();
if (s) {
px = s.x; py = s.y;
setHover(s.onCard ? s.el : null);
}
}
cursor.style.transform = `translate(${px}px, ${py}px)`;
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
})();
コード
HTML
<!-- ホバー拡大カーソル:要素ごとにラベルを差し替えながら円を拡大 -->
<div class="stage" data-scale-root>
<div class="content">
<h1 class="title">ホバー拡大カーソル</h1>
<p class="lead">カードに触れると、カーソルが拡大してラベルを表示します。</p>
<div class="cards">
<a class="card" data-hover data-label="VIEW">
<span class="card-emoji">🎨</span>
<span class="card-name">Gallery</span>
</a>
<a class="card" data-hover data-label="PLAY">
<span class="card-emoji">🎬</span>
<span class="card-name">Motion</span>
</a>
<a class="card" data-hover data-label="OPEN">
<span class="card-emoji">✨</span>
<span class="card-name">Effects</span>
</a>
</div>
</div>
<!-- 拡大カーソル(中にラベル) -->
<div class="scale-cursor" data-cursor>
<span class="scale-label" data-cursor-label></span>
</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 420px at 80% 10%, #3a2350 0%, transparent 60%),
#11101a;
color: #f1edff;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
cursor: none;
}
.content { text-align: center; padding: 22px; z-index: 1; }
.title {
margin: 0 0 8px;
font-size: clamp(26px, 5.5vw, 40px);
font-weight: 800;
letter-spacing: .02em;
color: #fff;
}
.lead { margin: 0 0 24px; color: #b3aacb; font-size: 13px; }
.cards { display: flex; gap: 18px; justify-content: center; flex-wrap: wrap; }
.card {
width: 120px; height: 130px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
border-radius: 18px;
text-decoration: none;
color: #e9e3ff;
background: rgba(255,255,255,.05);
border: 1px solid rgba(180,150,255,.22);
transition: transform .3s cubic-bezier(.2,.8,.2,1), background .3s ease, border-color .3s ease;
}
.card:hover {
transform: translateY(-6px);
background: rgba(180,150,255,.14);
border-color: rgba(180,150,255,.6);
}
.card-emoji { font-size: 34px; }
.card-name { font-size: 14px; font-weight: 600; letter-spacing: .04em; }
/* 拡大カーソル本体 */
.scale-cursor {
position: fixed;
top: 0; left: 0;
width: 18px; height: 18px;
border-radius: 50%;
background: rgba(180,150,255,.9);
display: grid;
place-items: center;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9999;
opacity: 0; /* 初回移動まで非表示(隅の点を見せない) */
transition: width .28s cubic-bezier(.2,.8,.2,1),
height .28s cubic-bezier(.2,.8,.2,1),
background .28s ease, opacity .3s ease;
will-change: transform;
}
[data-scale-root].is-active .scale-cursor { opacity: 1; }
/* ホバー時:円を大きくしてラベルを見せる */
.scale-cursor.is-hover {
width: 84px; height: 84px;
background: rgba(180,150,255,.95);
}
.scale-label {
font-size: 12px;
font-weight: 800;
letter-spacing: .12em;
color: #1b1330;
opacity: 0;
transform: scale(.6);
transition: opacity .22s ease, transform .22s ease;
}
.scale-cursor.is-hover .scale-label { opacity: 1; transform: scale(1); }
@media (prefers-reduced-motion: reduce) {
.card, .scale-cursor, .scale-label { transition: none; }
}
JavaScript
// ホバー拡大カーソル:要素に乗ると円を拡大し、data-labelの文字を表示
(() => {
const root = document.querySelector('[data-scale-root]');
const cursor = document.querySelector('[data-cursor]');
const label = document.querySelector('[data-cursor-label]');
if (!root || !cursor || !label) return; // null安全
// 円を即時追従
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');
});
// 領域外ではカーソルを隠し、ホバー状態も解除
root.addEventListener('pointerleave', () => {
root.classList.remove('is-active');
cursor.classList.remove('is-hover');
label.textContent = '';
});
// ホバー対象ごとに拡大+ラベル差し替え
document.querySelectorAll('[data-hover]').forEach((el) => {
el.addEventListener('pointerenter', () => {
cursor.classList.add('is-hover');
label.textContent = el.dataset.label || '';
});
el.addEventListener('pointerleave', () => {
cursor.classList.remove('is-hover');
label.textContent = '';
});
});
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「ホバー拡大カーソル」の効果を追加してください。
# 追加してほしい効果
ホバー拡大カーソル(カスタムカーソル)
要素ホバーで円が拡大し、data-labelの文字をカーソル内に表示。カードギャラリーやリンク集の操作ヒント提示に最適です。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ホバー拡大カーソル:要素ごとにラベルを差し替えながら円を拡大 -->
<div class="stage" data-scale-root>
<div class="content">
<h1 class="title">ホバー拡大カーソル</h1>
<p class="lead">カードに触れると、カーソルが拡大してラベルを表示します。</p>
<div class="cards">
<a class="card" data-hover data-label="VIEW">
<span class="card-emoji">🎨</span>
<span class="card-name">Gallery</span>
</a>
<a class="card" data-hover data-label="PLAY">
<span class="card-emoji">🎬</span>
<span class="card-name">Motion</span>
</a>
<a class="card" data-hover data-label="OPEN">
<span class="card-emoji">✨</span>
<span class="card-name">Effects</span>
</a>
</div>
</div>
<!-- 拡大カーソル(中にラベル) -->
<div class="scale-cursor" data-cursor>
<span class="scale-label" data-cursor-label></span>
</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 420px at 80% 10%, #3a2350 0%, transparent 60%),
#11101a;
color: #f1edff;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
cursor: none;
}
.content { text-align: center; padding: 22px; z-index: 1; }
.title {
margin: 0 0 8px;
font-size: clamp(26px, 5.5vw, 40px);
font-weight: 800;
letter-spacing: .02em;
color: #fff;
}
.lead { margin: 0 0 24px; color: #b3aacb; font-size: 13px; }
.cards { display: flex; gap: 18px; justify-content: center; flex-wrap: wrap; }
.card {
width: 120px; height: 130px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
border-radius: 18px;
text-decoration: none;
color: #e9e3ff;
background: rgba(255,255,255,.05);
border: 1px solid rgba(180,150,255,.22);
transition: transform .3s cubic-bezier(.2,.8,.2,1), background .3s ease, border-color .3s ease;
}
.card:hover {
transform: translateY(-6px);
background: rgba(180,150,255,.14);
border-color: rgba(180,150,255,.6);
}
.card-emoji { font-size: 34px; }
.card-name { font-size: 14px; font-weight: 600; letter-spacing: .04em; }
/* 拡大カーソル本体 */
.scale-cursor {
position: fixed;
top: 0; left: 0;
width: 18px; height: 18px;
border-radius: 50%;
background: rgba(180,150,255,.9);
display: grid;
place-items: center;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9999;
opacity: 0; /* 初回移動まで非表示(隅の点を見せない) */
transition: width .28s cubic-bezier(.2,.8,.2,1),
height .28s cubic-bezier(.2,.8,.2,1),
background .28s ease, opacity .3s ease;
will-change: transform;
}
[data-scale-root].is-active .scale-cursor { opacity: 1; }
/* ホバー時:円を大きくしてラベルを見せる */
.scale-cursor.is-hover {
width: 84px; height: 84px;
background: rgba(180,150,255,.95);
}
.scale-label {
font-size: 12px;
font-weight: 800;
letter-spacing: .12em;
color: #1b1330;
opacity: 0;
transform: scale(.6);
transition: opacity .22s ease, transform .22s ease;
}
.scale-cursor.is-hover .scale-label { opacity: 1; transform: scale(1); }
@media (prefers-reduced-motion: reduce) {
.card, .scale-cursor, .scale-label { transition: none; }
}
【JavaScript】
// ホバー拡大カーソル:要素に乗ると円を拡大し、data-labelの文字を表示
(() => {
const root = document.querySelector('[data-scale-root]');
const cursor = document.querySelector('[data-cursor]');
const label = document.querySelector('[data-cursor-label]');
if (!root || !cursor || !label) return; // null安全
// 円を即時追従
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');
});
// 領域外ではカーソルを隠し、ホバー状態も解除
root.addEventListener('pointerleave', () => {
root.classList.remove('is-active');
cursor.classList.remove('is-hover');
label.textContent = '';
});
// ホバー対象ごとに拡大+ラベル差し替え
document.querySelectorAll('[data-hover]').forEach((el) => {
el.addEventListener('pointerenter', () => {
cursor.classList.add('is-hover');
label.textContent = el.dataset.label || '';
});
el.addEventListener('pointerleave', () => {
cursor.classList.remove('is-hover');
label.textContent = '';
});
});
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。