マーカー注釈ツールチップ

本文中の赤いマーカー語にカーソルを当てると、その語の真上に画像つきの注釈カードがふわっと現れます。記事を読みながら補足情報へ自然に誘導でき、編集系メディアで効く仕掛けです。

#tooltip#annotation#hover#editorial

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- FlowDesk: 機能説明の用語に当てると、図版つき注釈が真上に出る -->
<div class="stage">
  <article class="doc" data-doc>
    <p class="brand">FlowDesk <span>features</span></p>
    <p>
      チームのタスクは<mark data-annot="0">リアルタイム同期</mark>で常に最新。
      メンバーごとの<mark data-annot="1">権限管理</mark>で、見せたい情報だけを安全に共有できます。
      週次の<mark data-annot="2">自動レポート</mark>が、進捗を勝手にまとめて届けるので、報告のための作業はもう要りません。
    </p>

    <div class="tip" data-tip aria-hidden="true">
      <div class="pic"><img alt="" loading="lazy"></div>
      <div class="cap"></div>
    </div>
  </article>
</div>
CSS
:root{ --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); }
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #eef3ff; }

.stage {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 100vh; max-height: 100%; padding: 24px;
  background: linear-gradient(180deg, #f5f8ff 0%, #e7eefb 100%);
}
.doc {
  position: relative; max-width: 560px; padding: 56px 28px 48px;
  font-family: system-ui, -apple-system, "Segoe UI", "Hiragino Kaku Gothic ProN", sans-serif;
  color: #1b2a4a; font-size: 15px; line-height: 2.15;
}
.doc p { margin: 0; }
.brand { font-weight: 800; font-size: 20px; color: #27407a; margin: 0 0 18px; letter-spacing: .02em; }
.brand span { font-size: 12px; font-weight: 600; letter-spacing: .26em; color: #8aa0c8; text-transform: uppercase; margin-left: 8px; }

mark {
  background: linear-gradient(transparent 60%, #4D7CFE55 60%);
  border-bottom: 3px solid #4D7CFE;
  color: inherit; padding: 0 1px; border-radius: 2px; cursor: help;
  transition: background .25s ease;
}
mark.active { background: #4D7CFE22; }

.tip {
  position: absolute; left: 0; top: 0;
  width: 220px; height: 150px; background: #fff; border-radius: 10px;
  box-shadow: 0 12px 30px rgba(28,46,90,.22);
  overflow: hidden; opacity: 0; transform: translateY(8px);
  pointer-events: none; z-index: 5;
  transition: opacity .2s ease, transform .2s ease;
}
.tip.on { opacity: 1; transform: translateY(0);
  transition: opacity .3s var(--ease-out-expo), transform .3s var(--ease-out-expo); }
.tip .pic { height: 110px; background: linear-gradient(135deg, #4D7CFE, #7AA0FF); }
.tip .pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tip .cap { height: 40px; display: flex; align-items: center; padding: 0 12px; font-size: 12px; color: #1b2a4a; }
JavaScript
// offsetベースの素朴な絶対配置+クランプ(デモと同じロジック。注釈内容のみテーマ差し替え)。
(() => {
  const doc = document.querySelector('[data-doc]');
  if (!doc) return;
  const marks  = [...doc.querySelectorAll('mark')];
  const tip    = doc.querySelector('[data-tip]');
  const tipImg = tip.querySelector('img');
  const tipCap = tip.querySelector('.cap');
  if (!marks.length || !tip) return;

  const data = [
    { seed: 'flowdesk-annot-1', cap: 'デバイス間の同期イメージ' },
    { seed: 'flowdesk-annot-2', cap: '権限マトリクスの例' },
    { seed: 'flowdesk-annot-3', cap: '自動生成レポート' }
  ];

  const TIP_W = 220, TIP_H = 150;
  let auto = true, i = 0, timer = 0, hideTimer = 0;

  const show = (idx) => {
    marks.forEach(m => m.classList.remove('active'));
    const m = marks[idx];
    m.classList.add('active');
    const d = data[idx] || data[0];
    tipImg.src = `https://picsum.photos/seed/${d.seed}/440/220`;
    tipCap.textContent = d.cap;
    const cx = m.offsetLeft + m.offsetWidth / 2;
    const left = Math.min(Math.max(8, cx - TIP_W / 2), doc.clientWidth - (TIP_W + 8));
    const top  = Math.max(6, m.offsetTop - 12 - TIP_H);
    tip.style.left = left + 'px';
    tip.style.top  = top + 'px';
    tip.classList.add('on');
  };
  const hide = () => { tip.classList.remove('on'); marks.forEach(m => m.classList.remove('active')); };

  const cycle = () => {
    if (!auto) return;
    show(i % marks.length); i++;
    clearTimeout(hideTimer);
    hideTimer = setTimeout(() => { if (auto) hide(); }, 1800);
  };
  const start = () => { clearInterval(timer); cycle(); timer = setInterval(cycle, 2500); };

  marks.forEach((m, idx) => {
    m.addEventListener('mouseenter', () => { auto = false; clearInterval(timer); clearTimeout(hideTimer); show(idx); });
    m.addEventListener('mouseleave', () => { hide(); auto = true; start(); });
  });
  tipImg.addEventListener('error', () => { tipImg.style.display = 'none'; });
  start();
})();

実装ガイド

使いどころ

編集系メディアや製品説明で、本文を読みながら用語の補足(画像つき注釈)へ自然に誘導したいときに。

実装時の注意点

位置はoffsetLeft/offsetTopベースの素朴な絶対配置で十分です(Popper等は不要)。left = clamp(8, 語中心-幅/2, コンテナ幅-幅-8) で見切れを防ぎます。疑似hoverで巡回しつつ、実hoverでタイマーを止め、離脱で再開。画像はpicsum+onerrorでグラデにフォールバックします。

対応ブラウザ

offset系・transform・transitionは全モダンブラウザで安定動作します。マウスhover前提の要素はタッチでタップ表示にするなどの配慮を入れ、対応バージョンは断定せず確認してください。

よくある失敗

markが行末で折り返すとoffsetWidthが複数行ぶんになり中心がずれます。上端でカードが見切れる場合はtopをクランプ。実hoverと自動巡回の競合に注意し、画像未読込でも成立するようフォールバックを必ず用意します。

応用例

注釈に動画やリンクを入れる、用語集と連動、ホバーで本文側もハイライト、ECで商品スペックの補足などに発展できます。

コード

HTML
<!-- マーカー注釈ツールチップ:本文の赤マーカー語に当てると画像つき注釈が真上に出る -->
<div class="stage">
  <article class="doc" data-doc>
    <p>
      動きのあるWebデザインは、ユーザーの<mark data-annot="0">視線誘導</mark>を自然に行い、
      情報を直感的に伝えます。過度な演出は避けつつ、適切な<mark data-annot="1">イージング</mark>と
      タイミングを選ぶことで、ブランドの世界観を損なわずに印象へ残せます。
      読みやすさと魅せ方のバランスを取りながら、必要な箇所にだけ
      <mark data-annot="2">アクセント</mark>を置くのが、洗練された設計の基本です。
    </p>

    <div class="tip" data-tip aria-hidden="true">
      <div class="pic"><img alt="" loading="lazy"></div>
      <div class="cap"></div>
    </div>
  </article>
</div>
CSS
:root{
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring:   cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-inout:    cubic-bezier(0.65, 0, 0.35, 1);
}
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #F7F5F0; }

.stage {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 100vh;
  max-height: 100%;
  padding: 24px;
  background: #F7F5F0;
}

.doc {
  position: relative;
  max-width: 560px;
  padding: 64px 28px 48px;
  font-family: system-ui, -apple-system, "Segoe UI", "Hiragino Kaku Gothic ProN", sans-serif;
  color: #1A1A1A;
  font-size: 15px;
  line-height: 2.15;
}
.doc p { margin: 0; }

mark {
  background: linear-gradient(transparent 60%, #FF5C5C55 60%);
  border-bottom: 3px solid #FF5C5C;
  color: inherit;
  padding: 0 1px;
  border-radius: 2px;
  cursor: help;
  transition: background .25s ease;
}
mark.active { background: #FF5C5C22; }

.tip {
  position: absolute;
  left: 0; top: 0;
  width: 220px; height: 150px;
  background: #fff;
  border-radius: 10px;
  box-shadow: 0 12px 30px rgba(0,0,0,.18);
  overflow: hidden;
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  z-index: 5;
  transition: opacity .2s ease, transform .2s ease;
}
.tip.on {
  opacity: 1;
  transform: translateY(0);
  transition: opacity .3s var(--ease-out-expo), transform .3s var(--ease-out-expo);
}
.tip .pic { height: 110px; background: linear-gradient(135deg, #4D7CFE, #9B6BFF); }
.tip .pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tip .cap { height: 40px; display: flex; align-items: center; padding: 0 12px; font-size: 12px; color: #1A1A1A; }
JavaScript
// offsetLeft/offsetTop ベースの素朴な絶対配置。位置はクランプ式で見切れを防ぐ。
(() => {
  const doc = document.querySelector('[data-doc]');
  if (!doc) return; // null安全
  const marks  = [...doc.querySelectorAll('mark')];
  const tip    = doc.querySelector('[data-tip]');
  const tipImg = tip.querySelector('img');
  const tipCap = tip.querySelector('.cap');
  if (!marks.length || !tip) return;

  const data = [
    { seed: 'wedelab-annot-1', cap: '視線を導く動きの実例図' },
    { seed: 'wedelab-annot-2', cap: 'イージング曲線の比較' },
    { seed: 'wedelab-annot-3', cap: 'アクセント配置のサンプル' }
  ];

  const TIP_W = 220, TIP_H = 150;
  let auto = true, i = 0, timer = 0, hideTimer = 0;

  const show = (idx) => {
    marks.forEach(m => m.classList.remove('active'));
    const m = marks[idx];
    m.classList.add('active');
    const d = data[idx] || data[0];
    tipImg.src = `https://picsum.photos/seed/${d.seed}/440/220`;
    tipCap.textContent = d.cap;

    const cx = m.offsetLeft + m.offsetWidth / 2;
    // left = clamp(8, 語中心-110, コンテナ幅-228)
    const left = Math.min(Math.max(8, cx - TIP_W / 2), doc.clientWidth - (TIP_W + 8));
    const top  = Math.max(6, m.offsetTop - 12 - TIP_H); // 語の真上12px、上端は6で止める
    tip.style.left = left + 'px';
    tip.style.top  = top + 'px';
    tip.classList.add('on');
  };
  const hide = () => { tip.classList.remove('on'); marks.forEach(m => m.classList.remove('active')); };

  const cycle = () => {
    if (!auto) return;
    show(i % marks.length);
    i++;
    clearTimeout(hideTimer);
    hideTimer = setTimeout(() => { if (auto) hide(); }, 1800);
  };
  const start = () => { clearInterval(timer); cycle(); timer = setInterval(cycle, 2500); };

  // 実hover:タイマー停止→表示、離れたら再開
  marks.forEach((m, idx) => {
    m.addEventListener('mouseenter', () => { auto = false; clearInterval(timer); clearTimeout(hideTimer); show(idx); });
    m.addEventListener('mouseleave', () => { hide(); auto = true; start(); });
  });
  tipImg.addEventListener('error', () => { tipImg.style.display = 'none'; }); // 画像失敗時はグラデ

  start();
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「マーカー注釈ツールチップ」の効果を追加してください。

# 追加してほしい効果
マーカー注釈ツールチップ(マイクロインタラクション)
本文中の赤いマーカー語にカーソルを当てると、その語の真上に画像つきの注釈カードがふわっと現れます。記事を読みながら補足情報へ自然に誘導でき、編集系メディアで効く仕掛けです。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- マーカー注釈ツールチップ:本文の赤マーカー語に当てると画像つき注釈が真上に出る -->
<div class="stage">
  <article class="doc" data-doc>
    <p>
      動きのあるWebデザインは、ユーザーの<mark data-annot="0">視線誘導</mark>を自然に行い、
      情報を直感的に伝えます。過度な演出は避けつつ、適切な<mark data-annot="1">イージング</mark>と
      タイミングを選ぶことで、ブランドの世界観を損なわずに印象へ残せます。
      読みやすさと魅せ方のバランスを取りながら、必要な箇所にだけ
      <mark data-annot="2">アクセント</mark>を置くのが、洗練された設計の基本です。
    </p>

    <div class="tip" data-tip aria-hidden="true">
      <div class="pic"><img alt="" loading="lazy"></div>
      <div class="cap"></div>
    </div>
  </article>
</div>

【CSS】
:root{
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring:   cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-inout:    cubic-bezier(0.65, 0, 0.35, 1);
}
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #F7F5F0; }

.stage {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 100vh;
  max-height: 100%;
  padding: 24px;
  background: #F7F5F0;
}

.doc {
  position: relative;
  max-width: 560px;
  padding: 64px 28px 48px;
  font-family: system-ui, -apple-system, "Segoe UI", "Hiragino Kaku Gothic ProN", sans-serif;
  color: #1A1A1A;
  font-size: 15px;
  line-height: 2.15;
}
.doc p { margin: 0; }

mark {
  background: linear-gradient(transparent 60%, #FF5C5C55 60%);
  border-bottom: 3px solid #FF5C5C;
  color: inherit;
  padding: 0 1px;
  border-radius: 2px;
  cursor: help;
  transition: background .25s ease;
}
mark.active { background: #FF5C5C22; }

.tip {
  position: absolute;
  left: 0; top: 0;
  width: 220px; height: 150px;
  background: #fff;
  border-radius: 10px;
  box-shadow: 0 12px 30px rgba(0,0,0,.18);
  overflow: hidden;
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  z-index: 5;
  transition: opacity .2s ease, transform .2s ease;
}
.tip.on {
  opacity: 1;
  transform: translateY(0);
  transition: opacity .3s var(--ease-out-expo), transform .3s var(--ease-out-expo);
}
.tip .pic { height: 110px; background: linear-gradient(135deg, #4D7CFE, #9B6BFF); }
.tip .pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tip .cap { height: 40px; display: flex; align-items: center; padding: 0 12px; font-size: 12px; color: #1A1A1A; }

【JavaScript】
// offsetLeft/offsetTop ベースの素朴な絶対配置。位置はクランプ式で見切れを防ぐ。
(() => {
  const doc = document.querySelector('[data-doc]');
  if (!doc) return; // null安全
  const marks  = [...doc.querySelectorAll('mark')];
  const tip    = doc.querySelector('[data-tip]');
  const tipImg = tip.querySelector('img');
  const tipCap = tip.querySelector('.cap');
  if (!marks.length || !tip) return;

  const data = [
    { seed: 'wedelab-annot-1', cap: '視線を導く動きの実例図' },
    { seed: 'wedelab-annot-2', cap: 'イージング曲線の比較' },
    { seed: 'wedelab-annot-3', cap: 'アクセント配置のサンプル' }
  ];

  const TIP_W = 220, TIP_H = 150;
  let auto = true, i = 0, timer = 0, hideTimer = 0;

  const show = (idx) => {
    marks.forEach(m => m.classList.remove('active'));
    const m = marks[idx];
    m.classList.add('active');
    const d = data[idx] || data[0];
    tipImg.src = `https://picsum.photos/seed/${d.seed}/440/220`;
    tipCap.textContent = d.cap;

    const cx = m.offsetLeft + m.offsetWidth / 2;
    // left = clamp(8, 語中心-110, コンテナ幅-228)
    const left = Math.min(Math.max(8, cx - TIP_W / 2), doc.clientWidth - (TIP_W + 8));
    const top  = Math.max(6, m.offsetTop - 12 - TIP_H); // 語の真上12px、上端は6で止める
    tip.style.left = left + 'px';
    tip.style.top  = top + 'px';
    tip.classList.add('on');
  };
  const hide = () => { tip.classList.remove('on'); marks.forEach(m => m.classList.remove('active')); };

  const cycle = () => {
    if (!auto) return;
    show(i % marks.length);
    i++;
    clearTimeout(hideTimer);
    hideTimer = setTimeout(() => { if (auto) hide(); }, 1800);
  };
  const start = () => { clearInterval(timer); cycle(); timer = setInterval(cycle, 2500); };

  // 実hover:タイマー停止→表示、離れたら再開
  marks.forEach((m, idx) => {
    m.addEventListener('mouseenter', () => { auto = false; clearInterval(timer); clearTimeout(hideTimer); show(idx); });
    m.addEventListener('mouseleave', () => { hide(); auto = true; start(); });
  });
  tipImg.addEventListener('error', () => { tipImg.style.display = 'none'; }); // 画像失敗時はグラデ

  start();
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。