ホバー拡大カーソル

要素ホバーで円が拡大し、data-labelの文字をカーソル内に表示。カードギャラリーやリンク集の操作ヒント提示に最適です。

#js#css#hover#interaction

ライブデモ

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