カーソル追従ツールチップ

作品エリア内でカーソルの動きに滑らかに追従するラベルが現れるツールチップ。項目ごとに文言が変わり、フォーカス時は静的なヒントで補います。

#css#js#tooltip

ライブデモ

コード

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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。