カーソル追従ツールチップ
作品エリア内でカーソルの動きに滑らかに追従するラベルが現れるツールチップ。項目ごとに文言が変わり、フォーカス時は静的なヒントで補います。
ライブデモ
コード
HTML
<!-- カーソル追従ツールチップ: 作品エリア内でカーソルに滑らかに追従するラベルが現れる。フォーカス時は項目の下に静的なヒントを表示 -->
<section class="fc-stage" aria-label="カーソル追従ツールチップの見本">
<p class="fc-eyebrow">FOLLOW CURSOR</p>
<h2 class="fc-title">作品にカーソルを重ねてみてください</h2>
<p class="fc-lead">エリア内でカーソルに寄り添うようにラベルが追いかけます。<kbd>Tab</kbd>で選ぶと項目の下にヒントが出ます。</p>
<div class="fc-field" data-role="field">
<div class="fc-item-wrap">
<button type="button" class="fc-item" data-label="見る →" aria-describedby="fc-static-1">
<span class="fc-item__art" aria-hidden="true">波</span>
<span class="fc-item__cap">波のかたち</span>
</button>
<div class="fc-static" id="fc-static-1" role="tooltip">静岡・三保 / 2023年</div>
</div>
<div class="fc-item-wrap">
<button type="button" class="fc-item" data-label="開く →" aria-describedby="fc-static-2">
<span class="fc-item__art" aria-hidden="true">灯</span>
<span class="fc-item__cap">灯りの午後</span>
</button>
<div class="fc-static" id="fc-static-2" role="tooltip">京都・町家 / 2024年</div>
</div>
<div class="fc-item-wrap">
<button type="button" class="fc-item" data-label="見る →" aria-describedby="fc-static-3">
<span class="fc-item__art" aria-hidden="true">庭</span>
<span class="fc-item__cap">静止した庭</span>
</button>
<div class="fc-static" id="fc-static-3" role="tooltip">奈良・古刹 / 2022年</div>
</div>
<div class="fc-cursor-tip" data-role="cursor-tip" aria-hidden="true">見る →</div>
</div>
</section>
CSS
/* カーソル追従ツールチップ: 生成りの作品エリアで、カーソルに黒檀のラベルがそっと寄り添う */
:root{
--bg-1:#f7f1e6;
--bg-2:#ecdfc6;
--surface:#fffdf9;
--line:#e6dac2;
--ink:#2a2420;
--muted:#8b7d6a;
--gold:#a9803f;
--gold-soft:#caa25f;
--tip-bg:#211b15;
--tip-ink:#f6efe1;
}
*{box-sizing:border-box}
body{
margin:0;
min-height:100vh;
display:grid;
place-items:center;
padding:44px 20px;
overflow-x:hidden;
overflow-y:auto;
font-family:"Segoe UI",system-ui,-apple-system,"Hiragino Kaku Gothic ProN",sans-serif;
color:var(--ink);
background:
radial-gradient(560px 300px at 12% 0%,#fffaf0 0%,transparent 60%),
radial-gradient(520px 280px at 100% 100%,#f2e2c4 0%,transparent 55%),
linear-gradient(180deg,var(--bg-1),var(--bg-2));
}
.fc-stage{
width:min(560px,100%);
background:var(--surface);
border:1px solid var(--line);
border-radius:20px;
box-shadow:0 26px 60px -30px rgba(60,42,10,.35),0 2px 10px rgba(60,42,10,.06);
padding:34px 30px 30px;
text-align:center;
}
.fc-eyebrow{margin:0 0 6px;font-size:.7rem;letter-spacing:.16em;color:var(--gold);font-weight:700}
.fc-title{margin:0 0 10px;font-size:1.24rem;font-family:Georgia,"Yu Mincho","Hiragino Mincho ProN",serif}
.fc-lead{margin:0 0 8px;font-size:.85rem;line-height:1.6;color:var(--muted)}
.fc-lead kbd{
font-family:inherit;font-size:.78em;padding:1px 6px;border-radius:5px;
background:#f1e9d8;border:1px solid var(--line);color:var(--ink);
}
.fc-field{
position:relative;
margin-top:26px;
display:flex;
flex-wrap:wrap;
justify-content:center;
gap:18px 16px;
padding:6px 6px 40px;
cursor:none;
}
.fc-item-wrap{position:relative}
.fc-item{
all:unset;
box-sizing:border-box;
display:flex;
flex-direction:column;
align-items:center;
gap:10px;
width:120px;
padding:16px 10px;
border-radius:14px;
border:1px solid var(--line);
background:#fbf6ec;
cursor:pointer;
transition:border-color .2s ease,transform .2s cubic-bezier(.22,1,.36,1),box-shadow .2s ease;
}
.fc-item:hover{border-color:var(--gold-soft);transform:translateY(-3px);box-shadow:0 14px 26px -16px rgba(120,90,30,.5)}
.fc-item:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
.fc-item__art{
width:44px;height:44px;border-radius:50%;
display:grid;place-items:center;
background:radial-gradient(circle at 35% 30%,#2f2419,var(--tip-bg));
color:var(--gold-soft);
font-family:Georgia,"Yu Mincho",serif;
font-size:1.05rem;
}
.fc-item__cap{font-size:.8rem;font-weight:600;color:var(--ink)}
.fc-static{
position:absolute;
left:50%;
top:calc(100% + 10px);
transform:translate(-50%,-4px);
width:max-content;
max-width:170px;
padding:8px 12px;
font-size:.72rem;
line-height:1.5;
color:var(--tip-ink);
background:var(--tip-bg);
border:1px solid rgba(201,162,92,.3);
border-radius:9px;
box-shadow:0 20px 40px -18px rgba(20,14,4,.6);
opacity:0;
pointer-events:none;
transition:opacity .18s ease,transform .26s cubic-bezier(.22,1,.36,1);
z-index:20;
}
.fc-static::before{
content:"";position:absolute;left:50%;top:-6px;transform:translateX(-50%);
border:6px solid transparent;border-bottom-color:var(--tip-bg);
}
.fc-item:focus-visible + .fc-static{opacity:1;transform:translate(-50%,0)}
.fc-cursor-tip{
position:absolute;
top:0;left:0;
padding:8px 14px;
font-size:.78rem;
font-weight:700;
white-space:nowrap;
color:var(--tip-ink);
background:var(--tip-bg);
border:1px solid rgba(201,162,92,.32);
border-radius:999px;
box-shadow:0 16px 34px -16px rgba(20,14,4,.6);
opacity:0;
pointer-events:none;
transition:opacity .15s ease;
z-index:60;
will-change:transform;
}
.fc-cursor-tip.is-visible{opacity:1}
@media (max-width:480px){
.fc-field{cursor:auto}
.fc-cursor-tip{display:none}
}
@media (prefers-reduced-motion:reduce){
.fc-item{transition:border-color .2s ease}
.fc-static{transition:opacity .12s ease}
}
JavaScript
// カーソル追従ツールチップ: エリア内でポインタが動くと、なめらかな遅延(lerp)を伴って
// ラベルがカーソルへ追従する。キーボード操作(フォーカス)では各項目の下に静的なヒントを表示する。
(() => {
const stage = document.querySelector('.fc-stage');
if (!stage) return; // null安全
const field = stage.querySelector('[data-role="field"]');
const cursorTip = stage.querySelector('[data-role="cursor-tip"]');
const items = Array.from(stage.querySelectorAll('.fc-item'));
if (!field || !cursorTip || !items.length) return;
// タッチ端末はホバー/カーソル追従の概念が無いため、静かに機能を無効化する
const isCoarsePointer = window.matchMedia('(pointer: coarse)').matches;
if (isCoarsePointer) return;
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const EASE = reduce ? 1 : 0.18;
const OFFSET_X = 20;
const OFFSET_Y = 18;
let targetX = 0, targetY = 0, curX = 0, curY = 0;
let raf = null;
let running = false;
const loop = () => {
curX += (targetX - curX) * EASE;
curY += (targetY - curY) * EASE;
cursorTip.style.transform = `translate3d(${curX}px, ${curY}px, 0)`;
if (running) raf = window.requestAnimationFrame(loop);
};
const start = () => {
if (running) return;
running = true;
raf = window.requestAnimationFrame(loop);
};
const stop = () => {
running = false;
if (raf) window.cancelAnimationFrame(raf);
raf = null;
};
field.addEventListener('pointermove', (e) => {
const rect = field.getBoundingClientRect();
const maxX = Math.max(rect.width - 10, 10);
const maxY = Math.max(rect.height - 10, 10);
targetX = Math.min(Math.max(e.clientX - rect.left + OFFSET_X, 10), maxX);
targetY = Math.min(Math.max(e.clientY - rect.top + OFFSET_Y, 10), maxY);
if (!running) { curX = targetX; curY = targetY; }
const hovered = e.target.closest('.fc-item');
cursorTip.textContent = hovered && hovered.dataset.label ? hovered.dataset.label : '見る →';
cursorTip.classList.add('is-visible');
start();
});
field.addEventListener('pointerleave', () => {
cursorTip.classList.remove('is-visible');
stop();
});
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「カーソル追従ツールチップ」の効果を追加してください。
# 追加してほしい効果
カーソル追従ツールチップ(ツールチップ & メニュー)
作品エリア内でカーソルの動きに滑らかに追従するラベルが現れるツールチップ。項目ごとに文言が変わり、フォーカス時は静的なヒントで補います。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- カーソル追従ツールチップ: 作品エリア内でカーソルに滑らかに追従するラベルが現れる。フォーカス時は項目の下に静的なヒントを表示 -->
<section class="fc-stage" aria-label="カーソル追従ツールチップの見本">
<p class="fc-eyebrow">FOLLOW CURSOR</p>
<h2 class="fc-title">作品にカーソルを重ねてみてください</h2>
<p class="fc-lead">エリア内でカーソルに寄り添うようにラベルが追いかけます。<kbd>Tab</kbd>で選ぶと項目の下にヒントが出ます。</p>
<div class="fc-field" data-role="field">
<div class="fc-item-wrap">
<button type="button" class="fc-item" data-label="見る →" aria-describedby="fc-static-1">
<span class="fc-item__art" aria-hidden="true">波</span>
<span class="fc-item__cap">波のかたち</span>
</button>
<div class="fc-static" id="fc-static-1" role="tooltip">静岡・三保 / 2023年</div>
</div>
<div class="fc-item-wrap">
<button type="button" class="fc-item" data-label="開く →" aria-describedby="fc-static-2">
<span class="fc-item__art" aria-hidden="true">灯</span>
<span class="fc-item__cap">灯りの午後</span>
</button>
<div class="fc-static" id="fc-static-2" role="tooltip">京都・町家 / 2024年</div>
</div>
<div class="fc-item-wrap">
<button type="button" class="fc-item" data-label="見る →" aria-describedby="fc-static-3">
<span class="fc-item__art" aria-hidden="true">庭</span>
<span class="fc-item__cap">静止した庭</span>
</button>
<div class="fc-static" id="fc-static-3" role="tooltip">奈良・古刹 / 2022年</div>
</div>
<div class="fc-cursor-tip" data-role="cursor-tip" aria-hidden="true">見る →</div>
</div>
</section>
【CSS】
/* カーソル追従ツールチップ: 生成りの作品エリアで、カーソルに黒檀のラベルがそっと寄り添う */
:root{
--bg-1:#f7f1e6;
--bg-2:#ecdfc6;
--surface:#fffdf9;
--line:#e6dac2;
--ink:#2a2420;
--muted:#8b7d6a;
--gold:#a9803f;
--gold-soft:#caa25f;
--tip-bg:#211b15;
--tip-ink:#f6efe1;
}
*{box-sizing:border-box}
body{
margin:0;
min-height:100vh;
display:grid;
place-items:center;
padding:44px 20px;
overflow-x:hidden;
overflow-y:auto;
font-family:"Segoe UI",system-ui,-apple-system,"Hiragino Kaku Gothic ProN",sans-serif;
color:var(--ink);
background:
radial-gradient(560px 300px at 12% 0%,#fffaf0 0%,transparent 60%),
radial-gradient(520px 280px at 100% 100%,#f2e2c4 0%,transparent 55%),
linear-gradient(180deg,var(--bg-1),var(--bg-2));
}
.fc-stage{
width:min(560px,100%);
background:var(--surface);
border:1px solid var(--line);
border-radius:20px;
box-shadow:0 26px 60px -30px rgba(60,42,10,.35),0 2px 10px rgba(60,42,10,.06);
padding:34px 30px 30px;
text-align:center;
}
.fc-eyebrow{margin:0 0 6px;font-size:.7rem;letter-spacing:.16em;color:var(--gold);font-weight:700}
.fc-title{margin:0 0 10px;font-size:1.24rem;font-family:Georgia,"Yu Mincho","Hiragino Mincho ProN",serif}
.fc-lead{margin:0 0 8px;font-size:.85rem;line-height:1.6;color:var(--muted)}
.fc-lead kbd{
font-family:inherit;font-size:.78em;padding:1px 6px;border-radius:5px;
background:#f1e9d8;border:1px solid var(--line);color:var(--ink);
}
.fc-field{
position:relative;
margin-top:26px;
display:flex;
flex-wrap:wrap;
justify-content:center;
gap:18px 16px;
padding:6px 6px 40px;
cursor:none;
}
.fc-item-wrap{position:relative}
.fc-item{
all:unset;
box-sizing:border-box;
display:flex;
flex-direction:column;
align-items:center;
gap:10px;
width:120px;
padding:16px 10px;
border-radius:14px;
border:1px solid var(--line);
background:#fbf6ec;
cursor:pointer;
transition:border-color .2s ease,transform .2s cubic-bezier(.22,1,.36,1),box-shadow .2s ease;
}
.fc-item:hover{border-color:var(--gold-soft);transform:translateY(-3px);box-shadow:0 14px 26px -16px rgba(120,90,30,.5)}
.fc-item:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
.fc-item__art{
width:44px;height:44px;border-radius:50%;
display:grid;place-items:center;
background:radial-gradient(circle at 35% 30%,#2f2419,var(--tip-bg));
color:var(--gold-soft);
font-family:Georgia,"Yu Mincho",serif;
font-size:1.05rem;
}
.fc-item__cap{font-size:.8rem;font-weight:600;color:var(--ink)}
.fc-static{
position:absolute;
left:50%;
top:calc(100% + 10px);
transform:translate(-50%,-4px);
width:max-content;
max-width:170px;
padding:8px 12px;
font-size:.72rem;
line-height:1.5;
color:var(--tip-ink);
background:var(--tip-bg);
border:1px solid rgba(201,162,92,.3);
border-radius:9px;
box-shadow:0 20px 40px -18px rgba(20,14,4,.6);
opacity:0;
pointer-events:none;
transition:opacity .18s ease,transform .26s cubic-bezier(.22,1,.36,1);
z-index:20;
}
.fc-static::before{
content:"";position:absolute;left:50%;top:-6px;transform:translateX(-50%);
border:6px solid transparent;border-bottom-color:var(--tip-bg);
}
.fc-item:focus-visible + .fc-static{opacity:1;transform:translate(-50%,0)}
.fc-cursor-tip{
position:absolute;
top:0;left:0;
padding:8px 14px;
font-size:.78rem;
font-weight:700;
white-space:nowrap;
color:var(--tip-ink);
background:var(--tip-bg);
border:1px solid rgba(201,162,92,.32);
border-radius:999px;
box-shadow:0 16px 34px -16px rgba(20,14,4,.6);
opacity:0;
pointer-events:none;
transition:opacity .15s ease;
z-index:60;
will-change:transform;
}
.fc-cursor-tip.is-visible{opacity:1}
@media (max-width:480px){
.fc-field{cursor:auto}
.fc-cursor-tip{display:none}
}
@media (prefers-reduced-motion:reduce){
.fc-item{transition:border-color .2s ease}
.fc-static{transition:opacity .12s ease}
}
【JavaScript】
// カーソル追従ツールチップ: エリア内でポインタが動くと、なめらかな遅延(lerp)を伴って
// ラベルがカーソルへ追従する。キーボード操作(フォーカス)では各項目の下に静的なヒントを表示する。
(() => {
const stage = document.querySelector('.fc-stage');
if (!stage) return; // null安全
const field = stage.querySelector('[data-role="field"]');
const cursorTip = stage.querySelector('[data-role="cursor-tip"]');
const items = Array.from(stage.querySelectorAll('.fc-item'));
if (!field || !cursorTip || !items.length) return;
// タッチ端末はホバー/カーソル追従の概念が無いため、静かに機能を無効化する
const isCoarsePointer = window.matchMedia('(pointer: coarse)').matches;
if (isCoarsePointer) return;
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const EASE = reduce ? 1 : 0.18;
const OFFSET_X = 20;
const OFFSET_Y = 18;
let targetX = 0, targetY = 0, curX = 0, curY = 0;
let raf = null;
let running = false;
const loop = () => {
curX += (targetX - curX) * EASE;
curY += (targetY - curY) * EASE;
cursorTip.style.transform = `translate3d(${curX}px, ${curY}px, 0)`;
if (running) raf = window.requestAnimationFrame(loop);
};
const start = () => {
if (running) return;
running = true;
raf = window.requestAnimationFrame(loop);
};
const stop = () => {
running = false;
if (raf) window.cancelAnimationFrame(raf);
raf = null;
};
field.addEventListener('pointermove', (e) => {
const rect = field.getBoundingClientRect();
const maxX = Math.max(rect.width - 10, 10);
const maxY = Math.max(rect.height - 10, 10);
targetX = Math.min(Math.max(e.clientX - rect.left + OFFSET_X, 10), maxX);
targetY = Math.min(Math.max(e.clientY - rect.top + OFFSET_Y, 10), maxY);
if (!running) { curX = targetX; curY = targetY; }
const hovered = e.target.closest('.fc-item');
cursorTip.textContent = hovered && hovered.dataset.label ? hovered.dataset.label : '見る →';
cursorTip.classList.add('is-visible');
start();
});
field.addEventListener('pointerleave', () => {
cursorTip.classList.remove('is-visible');
stop();
});
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。