ユーザーホバーカード

コメント欄の名前やアバターにカーソルを合わせると、肩書き・統計・フォローボタンを備えたプロフィールカードがふわっと現れます。

#css#js#popover

ライブデモ

コード

HTML
<!-- ユーザーホバーカード: 名前/アバターにカーソルを合わせる、またはフォーカスするとプロフィールカードがふわっと現れる -->
<section class="hc-stage" aria-label="ユーザーホバーカードの見本">
  <p class="hc-eyebrow">HOVERCARD</p>
  <h2 class="hc-title">コメント欄の名前から、人物像へ</h2>
  <p class="hc-lead">名前にカーソルを合わせる、または <kbd>Tab</kbd> で選ぶとプロフィールが現れます。</p>

  <ul class="hc-feed">
    <li class="hc-comment">
      <button type="button" class="hc-name" aria-haspopup="dialog" aria-expanded="false" aria-controls="hc-card-mio">
        <span class="hc-avatar" aria-hidden="true">澪</span>
        <span class="hc-name__text">水瀬 澪</span>
      </button>
      さんが「このやわらかい配色、好きです」とコメントしました。
    </li>
    <li class="hc-comment">
      <button type="button" class="hc-name" aria-haspopup="dialog" aria-expanded="false" aria-controls="hc-card-ren">
        <span class="hc-avatar hc-avatar--alt" aria-hidden="true">蓮</span>
        <span class="hc-name__text">霜月 蓮</span>
      </button>
      さんが「このレイアウト、参考にさせてください」とコメントしました。
    </li>
  </ul>

  <div class="hc-card" id="hc-card-mio" role="dialog" aria-modal="false" aria-labelledby="hc-card-mio-title" aria-hidden="true">
    <div class="hc-card__head">
      <span class="hc-card__avatar" aria-hidden="true">澪</span>
      <div class="hc-card__id">
        <h3 class="hc-card__name" id="hc-card-mio-title">水瀬 澪</h3>
        <p class="hc-card__role">プロダクトデザイナー / 京都</p>
      </div>
    </div>
    <p class="hc-card__bio">余白と色の呼吸を大切にしています。週末は器を集めています。</p>
    <dl class="hc-card__stats">
      <div><dt>投稿</dt><dd>128</dd></div>
      <div><dt>フォロワー</dt><dd>3,204</dd></div>
      <div><dt>フォロー中</dt><dd>96</dd></div>
    </dl>
    <button type="button" class="hc-follow" data-role="follow" aria-pressed="false" tabindex="-1">フォローする</button>
  </div>

  <div class="hc-card" id="hc-card-ren" role="dialog" aria-modal="false" aria-labelledby="hc-card-ren-title" aria-hidden="true">
    <div class="hc-card__head">
      <span class="hc-card__avatar hc-card__avatar--alt" aria-hidden="true">蓮</span>
      <div class="hc-card__id">
        <h3 class="hc-card__name" id="hc-card-ren-title">霜月 蓮</h3>
        <p class="hc-card__role">フロントエンドエンジニア / 東京</p>
      </div>
    </div>
    <p class="hc-card__bio">動きの気持ちよさを数値で詰めるのが好きです。猫を飼っています。</p>
    <dl class="hc-card__stats">
      <div><dt>投稿</dt><dd>342</dd></div>
      <div><dt>フォロワー</dt><dd>1,890</dd></div>
      <div><dt>フォロー中</dt><dd>211</dd></div>
    </dl>
    <button type="button" class="hc-follow" data-role="follow" aria-pressed="false" tabindex="-1">フォローする</button>
  </div>
</section>
CSS
/* ユーザーホバーカード: 生成りのコメント欄から、名前にhoverするとプロフィールカードがふわっと現れる */
:root{
  --bg-1:#f7f1e6;
  --bg-2:#ecdfc6;
  --surface:#fffdf9;
  --line:#e6dac2;
  --ink:#2a2420;
  --muted:#8b7d6a;
  --gold:#a9803f;
  --gold-soft:#caa25f;
  --tip-bg:#211b15;
}
*{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));
}

.hc-stage{
  position:relative;
  width:min(480px,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:32px 28px 36px;
}
.hc-eyebrow{margin:0 0 6px;font-size:.7rem;letter-spacing:.16em;color:var(--gold);font-weight:700;text-align:center}
.hc-title{margin:0 0 10px;font-size:1.22rem;text-align:center;font-family:Georgia,"Yu Mincho","Hiragino Mincho ProN",serif}
.hc-lead{margin:0 0 22px;font-size:.84rem;line-height:1.6;color:var(--muted);text-align:center}
.hc-lead kbd{
  font-family:inherit;font-size:.78em;padding:1px 6px;border-radius:5px;
  background:#f1e9d8;border:1px solid var(--line);color:var(--ink);
}

.hc-feed{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:14px}
.hc-comment{
  font-size:.9rem;line-height:1.9;color:var(--ink);
  padding:14px 16px;background:#fbf6ec;border:1px solid var(--line);border-radius:14px;
}

.hc-name{
  all:unset;
  display:inline-flex;
  align-items:center;
  gap:7px;
  cursor:pointer;
  padding:2px 8px 2px 2px;
  border-radius:999px;
  font-weight:700;
  vertical-align:middle;
  transition:background .15s ease;
}
.hc-name:hover,.hc-name:focus-visible,.hc-name[aria-expanded="true"]{background:rgba(169,128,63,.14)}
.hc-name:focus-visible{outline:2px solid var(--gold);outline-offset:2px}

.hc-avatar{
  width:26px;height:26px;border-radius:50%;
  display:grid;place-items:center;flex:0 0 auto;
  background:radial-gradient(circle at 35% 30%,#3a2c1c,var(--tip-bg));
  color:var(--gold-soft);
  font-family:Georgia,"Yu Mincho",serif;font-size:.82rem;
}
.hc-avatar--alt{background:radial-gradient(circle at 35% 30%,#233226,#12190f);color:#bcd1a8}
.hc-name__text{color:var(--ink)}

.hc-card{
  position:absolute;
  top:0;left:0;
  width:min(280px,84vw);
  padding:18px 18px 16px;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:18px;
  box-shadow:0 32px 64px -26px rgba(40,28,8,.42),0 4px 14px rgba(40,28,8,.08);
  opacity:0;
  transform:translateY(8px);
  pointer-events:none;
  transition:opacity .22s ease,transform .3s cubic-bezier(.22,1,.36,1);
  z-index:40;
  text-align:left;
}
.hc-card.is-open{opacity:1;transform:translateY(0);pointer-events:auto}
.hc-card::before{
  content:"";position:absolute;top:-6px;left:24px;
  border:6px solid transparent;border-bottom-color:var(--surface);
}

.hc-card__head{display:flex;align-items:center;gap:12px;margin-bottom:10px}
.hc-card__avatar{
  width:48px;height:48px;border-radius:50%;flex:0 0 auto;
  display:grid;place-items:center;
  background:radial-gradient(circle at 35% 30%,#3a2c1c,var(--tip-bg));
  color:var(--gold-soft);font-family:Georgia,"Yu Mincho",serif;font-size:1.3rem;
}
.hc-card__avatar--alt{background:radial-gradient(circle at 35% 30%,#233226,#12190f);color:#bcd1a8}
.hc-card__id{min-width:0}
.hc-card__name{margin:0;font-size:1rem;font-family:Georgia,"Yu Mincho",serif}
.hc-card__role{margin:2px 0 0;font-size:.76rem;color:var(--muted)}
.hc-card__bio{margin:0 0 14px;font-size:.8rem;line-height:1.6;color:var(--ink)}

.hc-card__stats{
  display:grid;grid-template-columns:repeat(3,1fr);
  gap:4px;margin:0 0 14px;padding:10px 0;
  border-top:1px solid var(--line);border-bottom:1px solid var(--line);
}
.hc-card__stats div{text-align:center}
.hc-card__stats dt{margin:0;font-size:.68rem;color:var(--muted)}
.hc-card__stats dd{margin:2px 0 0;font-size:.92rem;font-weight:700;color:var(--ink)}

.hc-follow{
  width:100%;
  font:inherit;font-weight:700;font-size:.82rem;
  color:#fff;
  background:linear-gradient(135deg,#3a2c1c,var(--tip-bg));
  border:1px solid var(--tip-bg);
  border-radius:10px;
  padding:9px 12px;
  cursor:pointer;
  transition:opacity .15s ease,transform .15s ease;
}
.hc-follow:hover{opacity:.9}
.hc-follow:active{transform:translateY(1px)}
.hc-follow:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
.hc-follow[aria-pressed="true"]{
  color:var(--ink);
  background:var(--surface);
  border-color:var(--line);
}

@media (prefers-reduced-motion:reduce){
  .hc-card{transition:opacity .15s ease}
}
JavaScript
// ユーザーホバーカード: 名前にhoverしてしばらく待つとプロフィールカードを表示。
// クリックで固定表示、フォーカスでも表示、Escや外側クリックで閉じる。フォローボタンはトグル動作。
(() => {
  const stage = document.querySelector('.hc-stage');
  if (!stage) return; // null安全

  const names = Array.from(stage.querySelectorAll('.hc-name'));
  if (!names.length) return;

  const SHOW_DELAY = 320;
  const HIDE_DELAY = 220;

  let showTimer = null;
  let hideTimer = null;
  let current = null; // { trigger, card, pinned }

  const clearTimers = () => {
    window.clearTimeout(showTimer);
    window.clearTimeout(hideTimer);
  };

  const position = (trigger, card) => {
    const tr = trigger.getBoundingClientRect();
    const sr = stage.getBoundingClientRect();
    const top = tr.bottom - sr.top + 12;
    const cardRect = card.getBoundingClientRect();
    const cardWidth = cardRect.width || 280;
    const margin = 14;
    const rawLeft = tr.left - sr.left - 8;
    const maxLeft = Math.max(margin, sr.width - cardWidth - margin);
    const left = Math.min(Math.max(rawLeft, margin), maxLeft);
    card.style.top = `${top}px`;
    card.style.left = `${left}px`;
  };

  const setInteractive = (card, on) => {
    const follow = card.querySelector('.hc-follow');
    if (follow) follow.tabIndex = on ? 0 : -1;
  };

  const closeCurrent = () => {
    if (!current) return;
    const { trigger, card } = current;
    if (card.contains(document.activeElement)) document.activeElement.blur();
    card.classList.remove('is-open');
    card.setAttribute('aria-hidden', 'true');
    trigger.setAttribute('aria-expanded', 'false');
    setInteractive(card, false);
    current = null;
  };

  const openFor = (trigger, card, { pinned = false } = {}) => {
    clearTimers();
    if (current && current.card !== card) closeCurrent();
    position(trigger, card);
    card.classList.add('is-open');
    card.setAttribute('aria-hidden', 'false');
    trigger.setAttribute('aria-expanded', 'true');
    setInteractive(card, true);
    current = { trigger, card, pinned };
  };

  names.forEach((trigger) => {
    const id = trigger.getAttribute('aria-controls');
    const card = id ? document.getElementById(id) : null;
    if (!card) return;

    const follow = card.querySelector('.hc-follow');
    if (follow) {
      follow.addEventListener('click', () => {
        const pressed = follow.getAttribute('aria-pressed') === 'true';
        follow.setAttribute('aria-pressed', String(!pressed));
        follow.textContent = !pressed ? 'フォロー中' : 'フォローする';
      });
    }

    trigger.addEventListener('mouseenter', () => {
      clearTimers();
      showTimer = window.setTimeout(() => openFor(trigger, card), SHOW_DELAY);
    });
    trigger.addEventListener('mouseleave', () => {
      window.clearTimeout(showTimer);
      if (current && current.pinned) return;
      hideTimer = window.setTimeout(closeCurrent, HIDE_DELAY);
    });
    trigger.addEventListener('focus', () => {
      clearTimers();
      openFor(trigger, card);
    });
    trigger.addEventListener('blur', () => {
      if (current && current.pinned) return;
      hideTimer = window.setTimeout(() => {
        if (!card.matches(':hover') && !card.contains(document.activeElement)) closeCurrent();
      }, HIDE_DELAY);
    });
    trigger.addEventListener('click', () => {
      clearTimers();
      if (current && current.card === card && current.pinned) {
        closeCurrent();
      } else {
        openFor(trigger, card, { pinned: true });
      }
    });

    card.addEventListener('mouseenter', () => window.clearTimeout(hideTimer));
    card.addEventListener('mouseleave', () => {
      if (current && current.pinned) return;
      hideTimer = window.setTimeout(closeCurrent, HIDE_DELAY);
    });
  });

  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && current) {
      const t = current.trigger;
      t.focus();
      closeCurrent();
    }
  });

  document.addEventListener('pointerdown', (e) => {
    if (!current) return;
    if (e.target.closest('.hc-name') || e.target.closest('.hc-card')) return;
    closeCurrent();
  });

  window.addEventListener('resize', () => {
    if (current) position(current.trigger, current.card);
  });

  // --- 待機時オートプレビュー: 誰も操作していない間、最初のカードを一度だけそっと見せる ---
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce || !names[0]) return;

  const firstTrigger = names[0];
  const firstCard = document.getElementById(firstTrigger.getAttribute('aria-controls'));
  let autoShown = false;
  const idleTimer = window.setTimeout(() => {
    if (autoShown || current || !firstCard) return;
    autoShown = true;
    openFor(firstTrigger, firstCard);
    window.setTimeout(() => {
      if (current && current.trigger === firstTrigger && !current.pinned) closeCurrent();
    }, 2400);
  }, 2800);
  stage.addEventListener('pointerenter', () => window.clearTimeout(idleTimer), { once: true });
})();

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

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

# 追加してほしい効果
ユーザーホバーカード(ツールチップ & メニュー)
コメント欄の名前やアバターにカーソルを合わせると、肩書き・統計・フォローボタンを備えたプロフィールカードがふわっと現れます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ユーザーホバーカード: 名前/アバターにカーソルを合わせる、またはフォーカスするとプロフィールカードがふわっと現れる -->
<section class="hc-stage" aria-label="ユーザーホバーカードの見本">
  <p class="hc-eyebrow">HOVERCARD</p>
  <h2 class="hc-title">コメント欄の名前から、人物像へ</h2>
  <p class="hc-lead">名前にカーソルを合わせる、または <kbd>Tab</kbd> で選ぶとプロフィールが現れます。</p>

  <ul class="hc-feed">
    <li class="hc-comment">
      <button type="button" class="hc-name" aria-haspopup="dialog" aria-expanded="false" aria-controls="hc-card-mio">
        <span class="hc-avatar" aria-hidden="true">澪</span>
        <span class="hc-name__text">水瀬 澪</span>
      </button>
      さんが「このやわらかい配色、好きです」とコメントしました。
    </li>
    <li class="hc-comment">
      <button type="button" class="hc-name" aria-haspopup="dialog" aria-expanded="false" aria-controls="hc-card-ren">
        <span class="hc-avatar hc-avatar--alt" aria-hidden="true">蓮</span>
        <span class="hc-name__text">霜月 蓮</span>
      </button>
      さんが「このレイアウト、参考にさせてください」とコメントしました。
    </li>
  </ul>

  <div class="hc-card" id="hc-card-mio" role="dialog" aria-modal="false" aria-labelledby="hc-card-mio-title" aria-hidden="true">
    <div class="hc-card__head">
      <span class="hc-card__avatar" aria-hidden="true">澪</span>
      <div class="hc-card__id">
        <h3 class="hc-card__name" id="hc-card-mio-title">水瀬 澪</h3>
        <p class="hc-card__role">プロダクトデザイナー / 京都</p>
      </div>
    </div>
    <p class="hc-card__bio">余白と色の呼吸を大切にしています。週末は器を集めています。</p>
    <dl class="hc-card__stats">
      <div><dt>投稿</dt><dd>128</dd></div>
      <div><dt>フォロワー</dt><dd>3,204</dd></div>
      <div><dt>フォロー中</dt><dd>96</dd></div>
    </dl>
    <button type="button" class="hc-follow" data-role="follow" aria-pressed="false" tabindex="-1">フォローする</button>
  </div>

  <div class="hc-card" id="hc-card-ren" role="dialog" aria-modal="false" aria-labelledby="hc-card-ren-title" aria-hidden="true">
    <div class="hc-card__head">
      <span class="hc-card__avatar hc-card__avatar--alt" aria-hidden="true">蓮</span>
      <div class="hc-card__id">
        <h3 class="hc-card__name" id="hc-card-ren-title">霜月 蓮</h3>
        <p class="hc-card__role">フロントエンドエンジニア / 東京</p>
      </div>
    </div>
    <p class="hc-card__bio">動きの気持ちよさを数値で詰めるのが好きです。猫を飼っています。</p>
    <dl class="hc-card__stats">
      <div><dt>投稿</dt><dd>342</dd></div>
      <div><dt>フォロワー</dt><dd>1,890</dd></div>
      <div><dt>フォロー中</dt><dd>211</dd></div>
    </dl>
    <button type="button" class="hc-follow" data-role="follow" aria-pressed="false" tabindex="-1">フォローする</button>
  </div>
</section>

【CSS】
/* ユーザーホバーカード: 生成りのコメント欄から、名前にhoverするとプロフィールカードがふわっと現れる */
:root{
  --bg-1:#f7f1e6;
  --bg-2:#ecdfc6;
  --surface:#fffdf9;
  --line:#e6dac2;
  --ink:#2a2420;
  --muted:#8b7d6a;
  --gold:#a9803f;
  --gold-soft:#caa25f;
  --tip-bg:#211b15;
}
*{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));
}

.hc-stage{
  position:relative;
  width:min(480px,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:32px 28px 36px;
}
.hc-eyebrow{margin:0 0 6px;font-size:.7rem;letter-spacing:.16em;color:var(--gold);font-weight:700;text-align:center}
.hc-title{margin:0 0 10px;font-size:1.22rem;text-align:center;font-family:Georgia,"Yu Mincho","Hiragino Mincho ProN",serif}
.hc-lead{margin:0 0 22px;font-size:.84rem;line-height:1.6;color:var(--muted);text-align:center}
.hc-lead kbd{
  font-family:inherit;font-size:.78em;padding:1px 6px;border-radius:5px;
  background:#f1e9d8;border:1px solid var(--line);color:var(--ink);
}

.hc-feed{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:14px}
.hc-comment{
  font-size:.9rem;line-height:1.9;color:var(--ink);
  padding:14px 16px;background:#fbf6ec;border:1px solid var(--line);border-radius:14px;
}

.hc-name{
  all:unset;
  display:inline-flex;
  align-items:center;
  gap:7px;
  cursor:pointer;
  padding:2px 8px 2px 2px;
  border-radius:999px;
  font-weight:700;
  vertical-align:middle;
  transition:background .15s ease;
}
.hc-name:hover,.hc-name:focus-visible,.hc-name[aria-expanded="true"]{background:rgba(169,128,63,.14)}
.hc-name:focus-visible{outline:2px solid var(--gold);outline-offset:2px}

.hc-avatar{
  width:26px;height:26px;border-radius:50%;
  display:grid;place-items:center;flex:0 0 auto;
  background:radial-gradient(circle at 35% 30%,#3a2c1c,var(--tip-bg));
  color:var(--gold-soft);
  font-family:Georgia,"Yu Mincho",serif;font-size:.82rem;
}
.hc-avatar--alt{background:radial-gradient(circle at 35% 30%,#233226,#12190f);color:#bcd1a8}
.hc-name__text{color:var(--ink)}

.hc-card{
  position:absolute;
  top:0;left:0;
  width:min(280px,84vw);
  padding:18px 18px 16px;
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:18px;
  box-shadow:0 32px 64px -26px rgba(40,28,8,.42),0 4px 14px rgba(40,28,8,.08);
  opacity:0;
  transform:translateY(8px);
  pointer-events:none;
  transition:opacity .22s ease,transform .3s cubic-bezier(.22,1,.36,1);
  z-index:40;
  text-align:left;
}
.hc-card.is-open{opacity:1;transform:translateY(0);pointer-events:auto}
.hc-card::before{
  content:"";position:absolute;top:-6px;left:24px;
  border:6px solid transparent;border-bottom-color:var(--surface);
}

.hc-card__head{display:flex;align-items:center;gap:12px;margin-bottom:10px}
.hc-card__avatar{
  width:48px;height:48px;border-radius:50%;flex:0 0 auto;
  display:grid;place-items:center;
  background:radial-gradient(circle at 35% 30%,#3a2c1c,var(--tip-bg));
  color:var(--gold-soft);font-family:Georgia,"Yu Mincho",serif;font-size:1.3rem;
}
.hc-card__avatar--alt{background:radial-gradient(circle at 35% 30%,#233226,#12190f);color:#bcd1a8}
.hc-card__id{min-width:0}
.hc-card__name{margin:0;font-size:1rem;font-family:Georgia,"Yu Mincho",serif}
.hc-card__role{margin:2px 0 0;font-size:.76rem;color:var(--muted)}
.hc-card__bio{margin:0 0 14px;font-size:.8rem;line-height:1.6;color:var(--ink)}

.hc-card__stats{
  display:grid;grid-template-columns:repeat(3,1fr);
  gap:4px;margin:0 0 14px;padding:10px 0;
  border-top:1px solid var(--line);border-bottom:1px solid var(--line);
}
.hc-card__stats div{text-align:center}
.hc-card__stats dt{margin:0;font-size:.68rem;color:var(--muted)}
.hc-card__stats dd{margin:2px 0 0;font-size:.92rem;font-weight:700;color:var(--ink)}

.hc-follow{
  width:100%;
  font:inherit;font-weight:700;font-size:.82rem;
  color:#fff;
  background:linear-gradient(135deg,#3a2c1c,var(--tip-bg));
  border:1px solid var(--tip-bg);
  border-radius:10px;
  padding:9px 12px;
  cursor:pointer;
  transition:opacity .15s ease,transform .15s ease;
}
.hc-follow:hover{opacity:.9}
.hc-follow:active{transform:translateY(1px)}
.hc-follow:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
.hc-follow[aria-pressed="true"]{
  color:var(--ink);
  background:var(--surface);
  border-color:var(--line);
}

@media (prefers-reduced-motion:reduce){
  .hc-card{transition:opacity .15s ease}
}

【JavaScript】
// ユーザーホバーカード: 名前にhoverしてしばらく待つとプロフィールカードを表示。
// クリックで固定表示、フォーカスでも表示、Escや外側クリックで閉じる。フォローボタンはトグル動作。
(() => {
  const stage = document.querySelector('.hc-stage');
  if (!stage) return; // null安全

  const names = Array.from(stage.querySelectorAll('.hc-name'));
  if (!names.length) return;

  const SHOW_DELAY = 320;
  const HIDE_DELAY = 220;

  let showTimer = null;
  let hideTimer = null;
  let current = null; // { trigger, card, pinned }

  const clearTimers = () => {
    window.clearTimeout(showTimer);
    window.clearTimeout(hideTimer);
  };

  const position = (trigger, card) => {
    const tr = trigger.getBoundingClientRect();
    const sr = stage.getBoundingClientRect();
    const top = tr.bottom - sr.top + 12;
    const cardRect = card.getBoundingClientRect();
    const cardWidth = cardRect.width || 280;
    const margin = 14;
    const rawLeft = tr.left - sr.left - 8;
    const maxLeft = Math.max(margin, sr.width - cardWidth - margin);
    const left = Math.min(Math.max(rawLeft, margin), maxLeft);
    card.style.top = `${top}px`;
    card.style.left = `${left}px`;
  };

  const setInteractive = (card, on) => {
    const follow = card.querySelector('.hc-follow');
    if (follow) follow.tabIndex = on ? 0 : -1;
  };

  const closeCurrent = () => {
    if (!current) return;
    const { trigger, card } = current;
    if (card.contains(document.activeElement)) document.activeElement.blur();
    card.classList.remove('is-open');
    card.setAttribute('aria-hidden', 'true');
    trigger.setAttribute('aria-expanded', 'false');
    setInteractive(card, false);
    current = null;
  };

  const openFor = (trigger, card, { pinned = false } = {}) => {
    clearTimers();
    if (current && current.card !== card) closeCurrent();
    position(trigger, card);
    card.classList.add('is-open');
    card.setAttribute('aria-hidden', 'false');
    trigger.setAttribute('aria-expanded', 'true');
    setInteractive(card, true);
    current = { trigger, card, pinned };
  };

  names.forEach((trigger) => {
    const id = trigger.getAttribute('aria-controls');
    const card = id ? document.getElementById(id) : null;
    if (!card) return;

    const follow = card.querySelector('.hc-follow');
    if (follow) {
      follow.addEventListener('click', () => {
        const pressed = follow.getAttribute('aria-pressed') === 'true';
        follow.setAttribute('aria-pressed', String(!pressed));
        follow.textContent = !pressed ? 'フォロー中' : 'フォローする';
      });
    }

    trigger.addEventListener('mouseenter', () => {
      clearTimers();
      showTimer = window.setTimeout(() => openFor(trigger, card), SHOW_DELAY);
    });
    trigger.addEventListener('mouseleave', () => {
      window.clearTimeout(showTimer);
      if (current && current.pinned) return;
      hideTimer = window.setTimeout(closeCurrent, HIDE_DELAY);
    });
    trigger.addEventListener('focus', () => {
      clearTimers();
      openFor(trigger, card);
    });
    trigger.addEventListener('blur', () => {
      if (current && current.pinned) return;
      hideTimer = window.setTimeout(() => {
        if (!card.matches(':hover') && !card.contains(document.activeElement)) closeCurrent();
      }, HIDE_DELAY);
    });
    trigger.addEventListener('click', () => {
      clearTimers();
      if (current && current.card === card && current.pinned) {
        closeCurrent();
      } else {
        openFor(trigger, card, { pinned: true });
      }
    });

    card.addEventListener('mouseenter', () => window.clearTimeout(hideTimer));
    card.addEventListener('mouseleave', () => {
      if (current && current.pinned) return;
      hideTimer = window.setTimeout(closeCurrent, HIDE_DELAY);
    });
  });

  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && current) {
      const t = current.trigger;
      t.focus();
      closeCurrent();
    }
  });

  document.addEventListener('pointerdown', (e) => {
    if (!current) return;
    if (e.target.closest('.hc-name') || e.target.closest('.hc-card')) return;
    closeCurrent();
  });

  window.addEventListener('resize', () => {
    if (current) position(current.trigger, current.card);
  });

  // --- 待機時オートプレビュー: 誰も操作していない間、最初のカードを一度だけそっと見せる ---
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce || !names[0]) return;

  const firstTrigger = names[0];
  const firstCard = document.getElementById(firstTrigger.getAttribute('aria-controls'));
  let autoShown = false;
  const idleTimer = window.setTimeout(() => {
    if (autoShown || current || !firstCard) return;
    autoShown = true;
    openFor(firstTrigger, firstCard);
    window.setTimeout(() => {
      if (current && current.trigger === firstTrigger && !current.pinned) closeCurrent();
    }, 2400);
  }, 2800);
  stage.addEventListener('pointerenter', () => window.clearTimeout(idleTimer), { once: true });
})();

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

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