Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5.5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="18.5" r="2.5"/><path d="m8.2 10.8 7.6-4.1M8.2 13.2l7.6 4.1"/></symbol>
  <symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6 6 18"/></symbol>
  <symbol id="i-chart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20V4M4 20h16M8 16v-5M12 16V8M16 16v-3"/></symbol>
  <symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></symbol>
</svg>

<div><span class="lbl">Hovercard on a name</span>
<div class="stage" style="--stage-h:330px">
  <ul class="feed"><li><span class="av" aria-hidden="true">MO</span><div>
    <small>Checkout: one-page address form · 2 hours ago</small>
    <p><span class="tt" id="hc-mara"><a class="who" href="#people/mara-okafor">Mara Okafor</a>
      <span class="tt-card">
        <span class="hc-head"><span class="hc-av" aria-hidden="true">MO</span><span><b>Mara Okafor</b><small>Design lead · Lisbon</small></span></span>
        <span class="hc-bio">Owns the checkout redesign and the icon library. Reviews design PRs on Tuesdays and Thursdays.</span>
        <span class="hc-foot"><span><svg width="14" height="14" aria-hidden="true" style="vertical-align:-2px"><use href="#i-clock"/></svg> 14:32 local time</span><a class="btn sm solid" href="#messages/mara-okafor">Message</a></span>
      </span></span> approved pull request #482</p></div></li></ul>
</div></div>

<div><span class="lbl">Rich tooltip with a title</span>
<div class="stage" style="--stage-h:330px">
  <ul class="apps">
    <li><span class="logo" aria-hidden="true">GD</span><span><b>Google Drive</b><small>Finance, Legal</small></span>
      <span class="tt" id="rt-drive"><button class="chip warn" type="button" aria-describedby="tip-drive"><i aria-hidden="true"></i>Sync paused</button>
      <span class="tt-tip tt-top" role="tooltip" id="tip-drive"><strong>Paused at 09:14</strong>Google Drive asked us to slow down. We retry automatically in 6 minutes.</span></span></li>
    <li><span class="logo" aria-hidden="true">N</span><span><b>Notion</b><small>3 workspaces</small></span>
      <span class="tt"><button class="chip ok" type="button" aria-describedby="tip-notion"><i aria-hidden="true"></i>Connected</button>
      <span class="tt-tip tt-top" role="tooltip" id="tip-notion"><strong>Read-only access</strong>Connected by Tom Reyes on 2 October. We read pages and never edit them.</span></span></li>
  </ul>
</div></div>

<div class="full"><span class="lbl">Popover that opens on click</span>
<div class="stage" style="--stage-h:290px">
  <div class="report">
    <span class="ic" aria-hidden="true"><svg><use href="#i-chart"/></svg></span>
    <span><b>Q3 revenue report</b><small>Updated 14 October by Finance</small></span>
    <button class="btn" type="button" id="share-btn" popovertarget="share-pop"><svg aria-hidden="true"><use href="#i-share"/></svg>Share</button>
  </div>
  <div class="pop" id="share-pop" popover role="dialog" aria-labelledby="share-title">
    <h3 id="share-title">Share Q3 revenue report</h3>
    <p>Anyone at Northwind with this link can view it. Links expire after 30 days.</p>
    <span class="pop-link">northwind.app/r/q3-revenue-8k2f</span>
    <div class="pop-acts"><button class="btn sm solid" type="button" data-copy="https://northwind.app/r/q3-revenue-8k2f">Copy link</button><p class="pop-note" role="status"></p></div>
    <button class="pop-x" type="button" popovertarget="share-pop" popovertargetaction="hide" aria-label="Close"><svg aria-hidden="true"><use href="#i-x"/></svg></button>
  </div>
</div></div>
CSS
/* Hovercard and rich tooltip: a .tt wrapper shows its card on hover (after a short delay) and on keyboard focus; the script
   adds Esc, tap to open and the window-edge nudge. Click popover: the popover attribute handles opening, Esc and outside clicks. */
.tt{--tt-bg:#14302c;--tt-ink:#f3faf8;--tt-gap:10px;--tt-arrow:6px;position:relative;display:inline-flex;vertical-align:middle}
.tt-tip,.tt-card{position:absolute;z-index:30;visibility:hidden;opacity:0;scale:0;transition:opacity .15s,visibility .15s,scale 0s .15s}
.tt:is(:hover,:focus-within,.is-open):not(.is-dismissed)>:is(.tt-tip,.tt-card){visibility:visible;opacity:1;scale:1;transition:opacity .15s,visibility 0s,scale 0s}
.tt:hover:not(:focus-within,.is-open,.is-dismissed)>.tt-card{transition:opacity .15s .25s,visibility .15s .25s,scale 0s .25s}

/* Rich tooltip: a title and a sentence, no controls inside */
.tt-tip{width:max-content;max-width:260px;padding:10px 12px;border-radius:10px;background:var(--tt-bg);color:var(--tt-ink);text-align:left;
  font:400 .8125rem/1.45 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;box-shadow:0 12px 28px -12px rgba(10,40,36,.6)}
.tt-tip strong{display:block;margin-bottom:2px;font-weight:650;font-size:.8438rem}
.tt-tip::before{content:"";position:absolute;border:var(--tt-arrow) solid transparent}
.tt-tip::after{content:"";position:absolute}
.tt-top{bottom:calc(100% + var(--tt-gap));left:50%;translate:calc(-50% + var(--tt-nudge,0px)) 0}
.tt-top::before{top:100%;left:calc(50% - var(--tt-arrow) - var(--tt-nudge,0px));border-top-color:var(--tt-bg);border-bottom-width:0}
.tt-top::after{top:100%;left:0;right:0;height:var(--tt-gap)}

/* Hovercard: a light card that can hold a link or a button */
.tt-card{top:calc(100% + 12px);left:0;translate:var(--tt-nudge,0px) 0;width:300px;max-width:calc(100vw - 32px);padding:16px;border:1px solid #dde5e3;border-radius:14px;
  background:#fff;color:#1b2b29;text-align:left;font-size:.875rem;line-height:1.5;box-shadow:0 2px 6px rgba(16,40,36,.06),0 24px 48px -16px rgba(16,40,36,.3)}
.tt-card::before{content:"";position:absolute;top:-7px;left:calc(22px - var(--tt-nudge,0px));width:12px;height:12px;background:#fff;border:solid #dde5e3;border-width:1px 0 0 1px;rotate:45deg}
.tt-card::after{content:"";position:absolute;left:0;right:0;bottom:100%;height:13px}
.hc-head{display:flex;align-items:center;gap:12px}
.hc-av{display:grid;place-items:center;flex:none;width:44px;height:44px;border-radius:50%;background:linear-gradient(135deg,#0f766e,#14b8a6);color:#fff;font-weight:700;font-size:.9rem}
.hc-head b{display:block;font-size:.95rem}
.hc-head small{display:block;color:#56635f;font-size:.8125rem}
.hc-bio{display:block;margin:12px 0 14px;color:#33433f}
.hc-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:12px;border-top:1px solid #e7eeec;color:#56635f;font-size:.8125rem}

/* Click popover */
.pop{position:fixed;inset:auto;margin:0;width:min(320px,calc(100vw - 16px));padding:18px;border:1px solid #dde5e3;border-radius:14px;background:#fff;color:#1b2b29;
  font-size:.875rem;line-height:1.5;box-shadow:0 2px 6px rgba(16,40,36,.06),0 24px 48px -16px rgba(16,40,36,.3);overflow:visible;
  opacity:0;translate:0 -4px;transition:opacity .15s,translate .15s,overlay .15s allow-discrete,display .15s allow-discrete}
.pop:popover-open{opacity:1;translate:0 0}
@starting-style{.pop:popover-open{opacity:0;translate:0 -4px}}
.pop::before{content:"";position:absolute;top:-7px;left:calc(var(--arrow-x,40px) - 6px);width:12px;height:12px;background:#fff;border:solid #dde5e3;border-width:1px 0 0 1px;rotate:45deg}
.pop h3{margin:0 28px 4px 0;font-size:.95rem}
.pop p{margin:0 0 12px;color:#33433f}
.pop-link{display:block;padding:8px 10px;border:1px solid #dde5e3;border-radius:8px;background:#f6f9f8;color:#1b2b29;font:500 .8125rem ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere}
.pop-acts{display:flex;align-items:center;gap:10px;margin-top:12px}
.pop-note{margin:0;color:#0f6b62;font-size:.8125rem;font-weight:600}
.pop-x{position:absolute;top:10px;right:10px;display:grid;place-items:center;width:30px;height:30px;padding:0;border:0;border-radius:8px;background:none;color:#56635f;cursor:pointer}
.pop-x:hover{background:#eef3f2;color:#1b2b29}
.pop-x svg{width:16px;height:16px}

/* Triggers and buttons */
.who{border-radius:4px;color:#0f6b62;font-weight:650;text-decoration:underline;text-decoration-color:rgba(15,107,98,.35);text-underline-offset:3px}
.who:hover{text-decoration-color:currentColor}
.chip{white-space:nowrap;display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border:1px solid transparent;border-radius:999px;font:600 .8125rem/1 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;cursor:help}
.chip i{width:7px;height:7px;border-radius:50%;background:currentColor}
.chip.warn{background:#fef3c7;border-color:#f3dc8b;color:#854d0e}
.chip.ok{background:#dcfce7;border-color:#b5e8c7;color:#166534}
.btn{display:inline-flex;align-items:center;gap:8px;height:36px;padding:0 14px;border:1px solid #c9d4d1;border-radius:9px;background:#fff;color:#1b2b29;
  font:600 .8125rem/1 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-decoration:none;cursor:pointer;transition:background-color .15s}
.btn:hover{background:#f1f5f4}
.btn.solid{background:#0f766e;border-color:#0f766e;color:#fff}
.btn.solid:hover{background:#0b5f58}
.btn.sm{height:30px;padding:0 12px}
.btn svg{width:16px;height:16px}
:is(.who,.chip,.btn,.pop-x):focus-visible{outline:2px solid #0f766e;outline-offset:2px}

/* Demo content */
.feed{margin:0;padding:0;list-style:none}
.feed li{display:flex;gap:12px;align-items:flex-start}
.feed .av{display:grid;place-items:center;flex:none;width:32px;height:32px;border-radius:50%;background:#ccebe6;color:#0f5f58;font-weight:700;font-size:.75rem}
.feed p{margin:2px 0 0;color:#33433f;font-size:.9375rem}
.feed small{display:block;color:#56635f;font-size:.8125rem}
.apps{margin:0;padding:84px 0 0;list-style:none}
.apps li{display:flex;align-items:center;gap:12px;padding:12px 0;border-top:1px solid #e7eeec}
.apps li:first-child{border-top:0}
.apps .logo{display:grid;place-items:center;flex:none;width:34px;height:34px;border-radius:9px;background:#f1f5f4;color:#1b2b29;font-weight:800;font-size:.8rem}
.apps b{display:block;font-size:.9rem}
.apps small{display:block;color:#56635f;font-size:.8125rem}
.apps .tt{margin-left:auto}
.report{display:flex;flex-wrap:wrap;align-items:center;gap:14px;padding:16px;border:1px solid #e1e8e6;border-radius:14px;background:#f8fbfa}
.report .ic{display:grid;place-items:center;flex:none;width:40px;height:40px;border-radius:10px;background:#0f766e;color:#fff}
.report .ic svg{width:20px;height:20px}
.report b{display:block}
.report small{display:block;color:#56635f;font-size:.8125rem}
.report .btn{margin-left:auto}
@media (prefers-reduced-motion:reduce){.tt :is(.tt-tip,.tt-card),.tt:is(:hover,:focus-within,.is-open)>:is(.tt-tip,.tt-card),.pop,.btn{transition:none!important}}
JavaScript
// Hovercards and tooltips (WCAG 1.4.13): Esc hides the open one and, if focus was inside a card, returns it to the trigger;
// a tap opens it on touch screens; opening one closes the others; cards and tooltips are nudged to stay inside the window.
(() => {
  const tts = [...document.querySelectorAll('.tt')];
  const fit = t => {
    const tip = t.querySelector(':scope > .tt-top, :scope > .tt-card'); if (!tip) return;
    tip.style.removeProperty('--tt-nudge');
    requestAnimationFrame(() => {
      const r = tip.getBoundingClientRect(), w = document.documentElement.clientWidth;
      const d = r.left < 8 ? 8 - r.left : r.right > w - 8 ? w - 8 - r.right : 0;
      if (d) tip.style.setProperty('--tt-nudge', d + 'px');
    });
  };
  const enter = t => { tts.forEach(o => o !== t && o.classList.remove('is-open')); t.classList.remove('is-dismissed'); fit(t); };
  tts.forEach(t => { t.addEventListener('pointerenter', () => enter(t)); t.addEventListener('focusin', () => enter(t)); if (t.classList.contains('is-open')) fit(t); });
  document.addEventListener('pointerdown', e => {
    const t = e.target.closest('.tt');
    if (!t) tts.forEach(o => o.classList.remove('is-open'));
    else if (e.pointerType === 'touch') t.classList.add('is-open');
  });
  document.addEventListener('keydown', e => {
    if (e.key !== 'Escape') return;
    tts.filter(t => t.matches(':hover, :focus-within, .is-open')).forEach(t => {
      const trigger = t.firstElementChild;
      if (t.contains(document.activeElement) && document.activeElement !== trigger) trigger.focus();
      t.classList.remove('is-open'); t.classList.add('is-dismissed');
    });
  });

  // Click popovers: place each one under its button (the popover attribute already handles Esc, outside clicks and focus).
  document.querySelectorAll('.pop[popover]').forEach(pop => {
    const btn = document.querySelector(`[popovertarget="${pop.id}"]`);
    const place = () => {
      const r = btn.getBoundingClientRect(), vw = document.documentElement.clientWidth, w = pop.offsetWidth;
      const left = Math.min(Math.max(8, r.left + r.width / 2 - w / 2), vw - w - 8);
      pop.style.left = left + 'px'; pop.style.top = r.bottom + 12 + 'px';
      pop.style.setProperty('--arrow-x', r.left + r.width / 2 - left + 'px');
    };
    pop.addEventListener('toggle', e => {
      const on = e.newState === 'open' ? addEventListener : removeEventListener;
      if (e.newState === 'open') place();
      on('scroll', place, true); on('resize', place);
    });
  });
  document.querySelectorAll('[data-copy]').forEach(b => b.addEventListener('click', () => {
    navigator.clipboard?.writeText(b.dataset.copy).catch(() => {});
    b.closest('.pop-acts').querySelector('[role="status"]').textContent = 'Link copied';
  }));
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5.5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="18.5" r="2.5"/><path d="m8.2 10.8 7.6-4.1M8.2 13.2l7.6 4.1"/></symbol>
  <symbol id="i-chart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20V4M4 20h16M8 16v-5M12 16V8M16 16v-3"/></symbol>
  <symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></symbol>
</svg>

<div><span class="lbl">Hovercard on a name</span>
<div class="stage" style="--stage-h:330px">
  <ul class="list-unstyled m-0"><li class="d-flex gap-3 align-items-start"><span class="logo rounded-circle" style="width:32px;height:32px;background:#ccebe6;color:#0f5f58;font-size:.75rem" aria-hidden="true">MO</span><div>
    <small class="d-block muted" style="font-size:.8125rem">Checkout: one-page address form · 2 hours ago</small>
    <p class="m-0" style="margin-top:2px!important;color:#33433f;font-size:.9375rem"><span class="tt" id="hc-mara"><a class="who" href="#people/mara-okafor">Mara Okafor</a>
      <span class="tt-card card"><span class="card-body d-block">
        <span class="d-flex align-items-center gap-3"><span class="hc-av rounded-circle" aria-hidden="true">MO</span><span><b class="d-block" style="font-size:.95rem">Mara Okafor</b><small class="d-block muted" style="font-size:.8125rem">Design lead · Lisbon</small></span></span>
        <span class="d-block mt-3 mb-3" style="color:#33433f">Owns the checkout redesign and the icon library. Reviews design PRs on Tuesdays and Thursdays.</span>
        <span class="d-flex align-items-center justify-content-between gap-2 pt-3 border-top muted" style="font-size:.8125rem;--bs-border-color:#e7eeec"><span><svg width="14" height="14" aria-hidden="true" style="vertical-align:-2px"><use href="#i-clock"/></svg> 14:32 local time</span><a class="btn btn-teal btn-sm solid" href="#messages/mara-okafor">Message</a></span>
      </span></span></span> approved pull request #482</p></div></li></ul>
</div></div>

<div><span class="lbl">Rich tooltip with a title</span>
<div class="stage" style="--stage-h:330px">
  <ul class="list-group list-group-flush" style="padding-top:84px;--bs-list-group-border-color:#e7eeec;--bs-list-group-item-padding-x:0;--bs-list-group-item-padding-y:12px">
    <li class="list-group-item d-flex align-items-center gap-3"><span class="logo" aria-hidden="true">GD</span><span><b class="d-block" style="font-size:.9rem">Google Drive</b><small class="d-block muted" style="font-size:.8125rem">Finance, Legal</small></span>
      <button class="badge rounded-pill chip warn ms-auto" type="button" id="rt-drive" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" data-bs-custom-class="tt-rich"
        data-bs-title="<strong>Paused at 09:14</strong>Google Drive asked us to slow down. We retry automatically in 6 minutes."><i aria-hidden="true"></i>Sync paused</button></li>
    <li class="list-group-item d-flex align-items-center gap-3"><span class="logo" aria-hidden="true">N</span><span><b class="d-block" style="font-size:.9rem">Notion</b><small class="d-block muted" style="font-size:.8125rem">3 workspaces</small></span>
      <button class="badge rounded-pill chip ok ms-auto" type="button" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" data-bs-custom-class="tt-rich"
        data-bs-title="<strong>Read-only access</strong>Connected by Tom Reyes on 2 October. We read pages and never edit them."><i aria-hidden="true"></i>Connected</button></li>
  </ul>
</div></div>

<div class="full"><span class="lbl">Popover that opens on click</span>
<div class="stage" style="--stage-h:290px">
  <div class="d-flex flex-wrap align-items-center gap-3 p-3 border rounded-4" style="--bs-border-color:#e1e8e6;background:#f8fbfa">
    <span class="report-ic" aria-hidden="true"><svg><use href="#i-chart"/></svg></span>
    <span><b class="d-block">Q3 revenue report</b><small class="d-block muted" style="font-size:.8125rem">Updated 14 October by Finance</small></span>
    <span class="ms-auto"><button class="btn btn-teal" type="button" id="share-btn" data-bs-toggle="popover" data-bs-placement="bottom" data-bs-title="Share Q3 revenue report" data-pop-content="share-pop"><svg aria-hidden="true"><use href="#i-share"/></svg>Share</button></span>
  </div>
  <template id="share-pop">
    <p>Anyone at Northwind with this link can view it. Links expire after 30 days.</p>
    <span class="pop-link">northwind.app/r/q3-revenue-8k2f</span>
    <div class="d-flex align-items-center gap-2 mt-3"><button class="btn btn-teal btn-sm solid" type="button" data-copy="https://northwind.app/r/q3-revenue-8k2f">Copy link</button><span class="pop-note" role="status"></span></div>
    <button class="btn-close" type="button" data-pop-close aria-label="Close"></button>
  </template>
</div></div>
CSS
/* Hovercard: Bootstrap's .card in a .tt wrapper, shown on hover (after a short delay) and keyboard focus.
   Rich tooltip: Bootstrap Tooltip with HTML content. Click popover: Bootstrap Popover. The script adds Esc and hover rules. */
.tt{position:relative;display:inline-flex;vertical-align:middle}
.tt-card{position:absolute;z-index:30;top:calc(100% + 12px);left:0;translate:var(--tt-nudge,0px) 0;width:300px;max-width:calc(100vw - 32px);
  --bs-card-border-color:#dde5e3;--bs-card-border-radius:14px;--bs-card-spacer-x:16px;--bs-card-spacer-y:16px;--bs-card-color:#1b2b29;
  text-align:left;font-size:.875rem;line-height:1.5;box-shadow:0 2px 6px rgba(16,40,36,.06),0 24px 48px -16px rgba(16,40,36,.3);
  visibility:hidden;opacity:0;scale:0;transition:opacity .15s,visibility .15s,scale 0s .15s}
.tt:is(:hover,:focus-within,.is-open):not(.is-dismissed)>.tt-card{visibility:visible;opacity:1;scale:1;transition:opacity .15s,visibility 0s,scale 0s}
.tt:hover:not(:focus-within,.is-open,.is-dismissed)>.tt-card{transition:opacity .15s .25s,visibility .15s .25s,scale 0s .25s}
.tt-card::before{content:"";position:absolute;top:-7px;left:calc(22px - var(--tt-nudge,0px));width:12px;height:12px;background:#fff;border:solid #dde5e3;border-width:1px 0 0 1px;rotate:45deg}
.tt-card::after{content:"";position:absolute;left:0;right:0;bottom:100%;height:13px}
.hc-av{display:grid;place-items:center;flex:none;width:44px;height:44px;background:linear-gradient(135deg,#0f766e,#14b8a6);color:#fff;font-weight:700;font-size:.9rem}

.tooltip.tt-rich{--bs-tooltip-bg:#14302c;--bs-tooltip-color:#f3faf8;--bs-tooltip-opacity:1;--bs-tooltip-max-width:260px;--bs-tooltip-padding-x:12px;--bs-tooltip-padding-y:10px;
  --bs-tooltip-font-size:.8125rem;--bs-tooltip-border-radius:10px;--bs-tooltip-arrow-width:12px;--bs-tooltip-arrow-height:6px;font-family:inherit}
.tooltip.tt-rich .tooltip-inner{text-align:left;line-height:1.45;box-shadow:0 12px 28px -12px rgba(10,40,36,.6)}
.tooltip.tt-rich strong{display:block;margin-bottom:2px;font-weight:650;font-size:.8438rem}

.popover.pop{--bs-popover-max-width:320px;--bs-popover-font-size:.875rem;--bs-popover-border-color:#dde5e3;--bs-popover-border-radius:14px;--bs-popover-inner-border-radius:13px;
  --bs-popover-header-bg:#fff;--bs-popover-header-color:#1b2b29;--bs-popover-header-font-size:.95rem;--bs-popover-header-padding-x:18px;--bs-popover-header-padding-y:18px;
  --bs-popover-body-padding-x:18px;--bs-popover-body-padding-y:0;--bs-popover-body-color:#33433f;--bs-popover-arrow-width:16px;--bs-popover-arrow-height:8px;
  width:min(320px,calc(100vw - 16px));font-family:inherit;line-height:1.5;box-shadow:0 2px 6px rgba(16,40,36,.06),0 24px 48px -16px rgba(16,40,36,.3)}
.popover.pop .popover-header{padding-bottom:4px;padding-right:46px;border-bottom:0;font-weight:700}
.popover.pop .popover-body{padding-bottom:18px}
.popover.pop p{margin:0 0 12px}
.popover.pop .btn-close{position:absolute;top:14px;right:14px;--bs-btn-close-focus-shadow:none}
.pop-link{display:block;padding:8px 10px;border:1px solid #dde5e3;border-radius:8px;background:#f6f9f8;color:#1b2b29;font:500 .8125rem ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere}
.pop-note{color:#0f6b62;font-size:.8125rem;font-weight:600}

/* Buttons and chips */
.btn-teal{--bs-btn-padding-x:14px;--bs-btn-font-size:.8125rem;--bs-btn-font-weight:600;--bs-btn-line-height:1;--bs-btn-border-radius:9px;--bs-btn-color:#1b2b29;--bs-btn-bg:#fff;
  --bs-btn-border-color:#c9d4d1;--bs-btn-hover-color:#1b2b29;--bs-btn-hover-bg:#f1f5f4;--bs-btn-hover-border-color:#c9d4d1;--bs-btn-active-color:#1b2b29;--bs-btn-active-bg:#e7eeec;
  --bs-btn-active-border-color:#c9d4d1;display:inline-flex;align-items:center;gap:8px;height:36px}
.btn-teal.solid{--bs-btn-color:#fff;--bs-btn-bg:#0f766e;--bs-btn-border-color:#0f766e;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5f58;--bs-btn-hover-border-color:#0b5f58;
  --bs-btn-active-color:#fff;--bs-btn-active-bg:#0b5f58;--bs-btn-active-border-color:#0b5f58}
.btn-teal.btn-sm{--bs-btn-padding-x:12px;height:30px}
.btn-teal svg{width:16px;height:16px}
.who{border-radius:4px;color:#0f6b62;font-weight:650;text-decoration-color:rgba(15,107,98,.35);text-underline-offset:3px}
.who:hover{color:#0f6b62;text-decoration-color:currentColor}
.chip{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border:1px solid transparent;font-size:.8125rem;font-weight:600;line-height:1;cursor:help}
.chip i{width:7px;height:7px;border-radius:50%;background:currentColor}
.chip.warn{background:#fef3c7;border-color:#f3dc8b;color:#854d0e}
.chip.ok{background:#dcfce7;border-color:#b5e8c7;color:#166534}
:is(.who,.chip,.btn-teal,.btn-close):focus-visible{outline:2px solid #0f766e;outline-offset:2px;box-shadow:none}
.logo{display:grid;place-items:center;flex:none;width:34px;height:34px;border-radius:9px;background:#f1f5f4;font-weight:800;font-size:.8rem}
.report-ic{display:grid;place-items:center;flex:none;width:40px;height:40px;border-radius:10px;background:#0f766e;color:#fff}
.report-ic svg{width:20px;height:20px}
.muted{color:#56635f}
/* Bootstrap's fade is off (animation: false avoids a show/hide race when Esc is pressed mid-fade); this fades it in instead. */
.tooltip.show{animation:tt-in .15s ease-out}
@keyframes tt-in{from{opacity:0}}
@media (prefers-reduced-motion:reduce){.tooltip.show{animation:none}.tt .tt-card,.tt:is(:hover,:focus-within,.is-open)>.tt-card,.tooltip,.popover,.btn{transition:none!important}}
JavaScript
(() => {
  // Hovercards (WCAG 1.4.13): Esc hides the card and returns focus to its link; a tap opens it on touch screens; nudged to stay on screen.
  const tts = [...document.querySelectorAll('.tt')];
  const fit = t => {
    const card = t.querySelector(':scope > .tt-card'); if (!card) return;
    card.style.removeProperty('--tt-nudge');
    requestAnimationFrame(() => {
      const r = card.getBoundingClientRect(), w = document.documentElement.clientWidth;
      const d = r.left < 8 ? 8 - r.left : r.right > w - 8 ? w - 8 - r.right : 0;
      if (d) card.style.setProperty('--tt-nudge', d + 'px');
    });
  };
  const enter = t => { tts.forEach(o => o !== t && o.classList.remove('is-open')); t.classList.remove('is-dismissed'); fit(t); };
  tts.forEach(t => { t.addEventListener('pointerenter', () => enter(t)); t.addEventListener('focusin', () => enter(t)); if (t.classList.contains('is-open')) fit(t); });

  // Bootstrap Tooltip, triggered here so it opens on hover, focus and tap, stays open while the pointer is on it and closes with Esc.
  const tips = new Map();
  const hideTips = keep => tips.forEach(t => t !== keep && t.hide());
  document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
    const tip = new bootstrap.Tooltip(el, { trigger: 'manual', animation: false, offset: [0, 10], customClass: el.dataset.bsCustomClass || '' });
    tips.set(el, tip);
    let open = false, timer;
    el.addEventListener('show.bs.tooltip', () => { open = true; });
    el.addEventListener('hide.bs.tooltip', () => { open = false; });
    el.addEventListener('inserted.bs.tooltip', () => {
      tip.tip.addEventListener('mouseenter', () => clearTimeout(timer));
      tip.tip.addEventListener('mouseleave', leave);
    });
    const show = () => { clearTimeout(timer); hideTips(tip); tts.forEach(o => o.classList.remove('is-open')); if (!open) tip.show(); };
    const leave = () => { clearTimeout(timer); timer = setTimeout(() => { if (el !== document.activeElement) tip.hide(); }, 150); };
    el.addEventListener('mouseenter', show);
    el.addEventListener('focus', () => { clearTimeout(timer); if (!open) tip.show(); });
    el.addEventListener('mouseleave', leave);
    el.addEventListener('blur', () => { if (!el.matches(':hover')) tip.hide(); });
  });

  // Bootstrap Popover opened by click. Its content comes from a <template>; Esc, the close button and outside clicks close it.
  const pops = [];
  document.querySelectorAll('[data-bs-toggle="popover"]').forEach(btn => {
    const pop = new bootstrap.Popover(btn, {
      trigger: 'click', html: true, container: btn.parentElement, offset: [0, 12], customClass: 'pop',
      template: `<div class="popover" role="dialog" aria-labelledby="${btn.id}-title"><div class="popover-arrow"></div><h3 class="popover-header" id="${btn.id}-title"></h3><div class="popover-body"></div></div>`,
      content: () => document.getElementById(btn.dataset.popContent).content.cloneNode(true)
    });
    pops.push([btn, pop]);
    btn.setAttribute('aria-expanded', 'false');
    btn.addEventListener('shown.bs.popover', () => { btn.setAttribute('aria-expanded', 'true'); btn.removeAttribute('aria-describedby'); });
    btn.addEventListener('hidden.bs.popover', () => btn.setAttribute('aria-expanded', 'false'));
  });
  const closePops = (except, refocus) => pops.forEach(([btn, pop]) => {
    if (btn === except || btn.getAttribute('aria-expanded') !== 'true') return;
    pop.hide(); if (refocus) btn.focus();
  });
  document.addEventListener('click', e => {
    const x = e.target.closest('[data-pop-close]');
    if (x) closePops(null, true);
    const c = e.target.closest('[data-copy]');
    if (c) { navigator.clipboard?.writeText(c.dataset.copy).catch(() => {}); c.parentElement.querySelector('[role="status"]').textContent = 'Link copied'; }
  });

  document.addEventListener('pointerdown', e => {
    const t = e.target.closest('.tt');
    if (!t) tts.forEach(o => o.classList.remove('is-open')); else if (e.pointerType === 'touch') t.classList.add('is-open');
    if (![...tips.keys()].some(el => el.contains(e.target)) && !e.target.closest('.tooltip')) hideTips();
    if (!e.target.closest('.popover, [data-bs-toggle="popover"]')) closePops();
  });
  document.addEventListener('keydown', e => {
    if (e.key !== 'Escape') return;
    hideTips();
    const inPop = document.activeElement.closest('.popover');
    closePops(null, true);
    if (inPop) return;
    tts.filter(t => t.matches(':hover, :focus-within, .is-open')).forEach(t => {
      const trigger = t.firstElementChild;
      if (t.contains(document.activeElement) && document.activeElement !== trigger) trigger.focus();
      t.classList.remove('is-open'); t.classList.add('is-dismissed');
    });
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5.5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="18.5" r="2.5"/><path d="m8.2 10.8 7.6-4.1M8.2 13.2l7.6 4.1"/></symbol>
  <symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6 6 18"/></symbol>
  <symbol id="i-chart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20V4M4 20h16M8 16v-5M12 16V8M16 16v-3"/></symbol>
  <symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></symbol>
</svg>

<div><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#56635f] uppercase">Hovercard on a name</span>
<div class="rounded-[18px] border border-[#dbe3e1] bg-white p-7 max-[600px]:p-5 min-h-[330px]">
  <ul class="m-0 list-none p-0"><li class="flex items-start gap-3"><span class="grid size-8 flex-none place-items-center rounded-full bg-[#ccebe6] text-xs font-bold text-[#0f5f58]" aria-hidden="true">MO</span><div>
    <small class="block text-[.8125rem] text-[#56635f]">Checkout: one-page address form · 2 hours ago</small>
    <p class="mt-0.5 mb-0 text-[.9375rem] text-[#33433f]"><span class="tt relative inline-flex align-middle" id="hc-mara"><a class="rounded font-[650] text-[#0f6b62] underline decoration-[rgba(15,107,98,.35)] underline-offset-[3px] hover:decoration-current focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f766e]" href="#people/mara-okafor">Mara Okafor</a>
      <span class="tt-card absolute top-[calc(100%+12px)] left-0 z-30 w-[300px] max-w-[calc(100vw-32px)] translate-x-[var(--tt-nudge,0px)] rounded-[14px] border border-[#dde5e3] bg-white p-4 text-left text-sm leading-normal text-[#1b2b29] shadow-[0_2px_6px_rgba(16,40,36,.06),0_24px_48px_-16px_rgba(16,40,36,.3)] invisible opacity-0 scale-0 tt-open:visible tt-open:opacity-100 tt-open:scale-100 motion-safe:[transition:opacity_.15s,visibility_.15s,scale_0s_.15s] motion-safe:tt-open:[transition:opacity_.15s,visibility_0s,scale_0s] motion-safe:tt-hover:[transition:opacity_.15s_.25s,visibility_.15s_.25s,scale_0s_.25s] before:absolute before:-top-[7px] before:left-[calc(22px-var(--tt-nudge,0px))] before:size-3 before:rotate-45 before:border-t before:border-l before:border-[#dde5e3] before:bg-white after:absolute after:inset-x-0 after:bottom-full after:h-[13px]">
        <span class="flex items-center gap-3"><span class="grid size-11 flex-none place-items-center rounded-full bg-linear-135 from-[#0f766e] to-[#14b8a6] text-[.9rem] font-bold text-white" aria-hidden="true">MO</span><span><b class="block text-[.95rem]">Mara Okafor</b><small class="block text-[.8125rem] text-[#56635f]">Design lead · Lisbon</small></span></span>
        <span class="mt-3 mb-3.5 block text-[#33433f]">Owns the checkout redesign and the icon library. Reviews design PRs on Tuesdays and Thursdays.</span>
        <span class="flex items-center justify-between gap-2.5 border-t border-[#e7eeec] pt-3 text-[.8125rem] text-[#56635f]"><span><svg class="inline size-3.5 align-[-2px]" aria-hidden="true"><use href="#i-clock"/></svg> 14:32 local time</span><a class="inline-flex h-[30px] cursor-pointer items-center rounded-[9px] border border-[#0f766e] bg-[#0f766e] px-3 text-[.8125rem] leading-none font-semibold text-white no-underline transition-colors duration-150 hover:bg-[#0b5f58] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f766e]" href="#messages/mara-okafor">Message</a></span>
      </span></span> approved pull request #482</p></div></li></ul>
</div></div>

<div><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#56635f] uppercase">Rich tooltip with a title</span>
<div class="rounded-[18px] border border-[#dbe3e1] bg-white p-7 max-[600px]:p-5 min-h-[330px]">
  <ul class="m-0 list-none p-0 pt-[84px]">
    <li class="flex items-center gap-3 py-3"><span class="grid size-[34px] flex-none place-items-center rounded-[9px] bg-[#f1f5f4] text-[.8rem] font-extrabold" aria-hidden="true">GD</span><span><b class="block text-[.9rem]">Google Drive</b><small class="block text-[.8125rem] text-[#56635f]">Finance, Legal</small></span>
      <span class="tt relative ml-auto inline-flex align-middle" id="rt-drive"><button class="inline-flex h-7 cursor-help items-center gap-1.5 rounded-full border px-2.5 text-[.8125rem] leading-none font-semibold whitespace-nowrap focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f766e] border-[#f3dc8b] bg-[#fef3c7] text-[#854d0e]" type="button" aria-describedby="tip-drive"><i class="size-[7px] rounded-full bg-current" aria-hidden="true"></i>Sync paused</button>
      <span class="pointer-events-auto absolute z-30 w-max max-w-[260px] rounded-[10px] bg-tt px-3 py-2.5 text-[.8125rem] leading-[1.45] font-normal text-tt-ink whitespace-normal shadow-[0_12px_28px_-12px_rgba(10,40,36,.6)] before:absolute before:border-transparent after:absolute before:border-[6px] invisible opacity-0 scale-0 tt-open:visible tt-open:opacity-100 tt-open:scale-100 motion-safe:[transition:opacity_.15s,visibility_.15s,scale_0s_.15s] motion-safe:tt-open:[transition:opacity_.15s,visibility_0s,scale_0s] tt-top bottom-[calc(100%+10px)] left-1/2 translate-x-[calc(-50%+var(--tt-nudge,0px))] before:top-full before:left-[calc(50%-6px-var(--tt-nudge,0px))] before:border-b-0 before:border-t-tt after:inset-x-0 after:top-full after:h-[10px] text-left" role="tooltip" id="tip-drive"><strong class="mb-0.5 block text-[.8438rem] font-[650]">Paused at 09:14</strong>Google Drive asked us to slow down. We retry automatically in 6 minutes.</span></span></li>
    <li class="flex items-center gap-3 border-t border-[#e7eeec] py-3"><span class="grid size-[34px] flex-none place-items-center rounded-[9px] bg-[#f1f5f4] text-[.8rem] font-extrabold" aria-hidden="true">N</span><span><b class="block text-[.9rem]">Notion</b><small class="block text-[.8125rem] text-[#56635f]">3 workspaces</small></span>
      <span class="tt relative ml-auto inline-flex align-middle"><button class="inline-flex h-7 cursor-help items-center gap-1.5 rounded-full border px-2.5 text-[.8125rem] leading-none font-semibold whitespace-nowrap focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f766e] border-[#b5e8c7] bg-[#dcfce7] text-[#166534]" type="button" aria-describedby="tip-notion"><i class="size-[7px] rounded-full bg-current" aria-hidden="true"></i>Connected</button>
      <span class="pointer-events-auto absolute z-30 w-max max-w-[260px] rounded-[10px] bg-tt px-3 py-2.5 text-[.8125rem] leading-[1.45] font-normal text-tt-ink whitespace-normal shadow-[0_12px_28px_-12px_rgba(10,40,36,.6)] before:absolute before:border-transparent after:absolute before:border-[6px] invisible opacity-0 scale-0 tt-open:visible tt-open:opacity-100 tt-open:scale-100 motion-safe:[transition:opacity_.15s,visibility_.15s,scale_0s_.15s] motion-safe:tt-open:[transition:opacity_.15s,visibility_0s,scale_0s] tt-top bottom-[calc(100%+10px)] left-1/2 translate-x-[calc(-50%+var(--tt-nudge,0px))] before:top-full before:left-[calc(50%-6px-var(--tt-nudge,0px))] before:border-b-0 before:border-t-tt after:inset-x-0 after:top-full after:h-[10px] text-left" role="tooltip" id="tip-notion"><strong class="mb-0.5 block text-[.8438rem] font-[650]">Read-only access</strong>Connected by Tom Reyes on 2 October. We read pages and never edit them.</span></span></li>
  </ul>
</div></div>

<div class="col-span-full"><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#56635f] uppercase">Popover that opens on click</span>
<div class="rounded-[18px] border border-[#dbe3e1] bg-white p-7 max-[600px]:p-5 min-h-[290px]">
  <div class="flex flex-wrap items-center gap-3.5 rounded-[14px] border border-[#e1e8e6] bg-[#f8fbfa] p-4">
    <span class="grid size-10 flex-none place-items-center rounded-[10px] bg-[#0f766e] text-white" aria-hidden="true"><svg class="size-5"><use href="#i-chart"/></svg></span>
    <span><b class="block">Q3 revenue report</b><small class="block text-[.8125rem] text-[#56635f]">Updated 14 October by Finance</small></span>
    <button class="inline-flex h-9 cursor-pointer items-center gap-2 rounded-[9px] border border-[#c9d4d1] bg-white px-3.5 text-[.8125rem] leading-none font-semibold text-[#1b2b29] no-underline transition-colors duration-150 hover:bg-[#f1f5f4] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f766e] ml-auto" type="button" id="share-btn" popovertarget="share-pop"><svg class="size-4" aria-hidden="true"><use href="#i-share"/></svg>Share</button>
  </div>
  <div class="pop fixed inset-auto m-0 w-[min(320px,calc(100vw-16px))] overflow-visible rounded-[14px] border border-[#dde5e3] bg-white p-[18px] text-sm leading-normal text-[#1b2b29] shadow-[0_2px_6px_rgba(16,40,36,.06),0_24px_48px_-16px_rgba(16,40,36,.3)] opacity-0 -translate-y-1 open:opacity-100 open:translate-y-0 starting:open:opacity-0 starting:open:-translate-y-1 transition-[opacity,translate,overlay,display] transition-discrete duration-150 motion-reduce:transition-none before:absolute before:-top-[7px] before:left-[calc(var(--arrow-x,40px)-6px)] before:size-3 before:rotate-45 before:border-t before:border-l before:border-[#dde5e3] before:bg-white" id="share-pop" popover role="dialog" aria-labelledby="share-title">
    <h3 class="mt-0 mr-7 mb-1 text-[.95rem] font-bold" id="share-title">Share Q3 revenue report</h3>
    <p class="mt-0 mb-3 text-[#33433f]">Anyone at Northwind with this link can view it. Links expire after 30 days.</p>
    <span class="block rounded-lg border border-[#dde5e3] bg-[#f6f9f8] px-2.5 py-2 font-mono text-[.8125rem] font-medium wrap-anywhere">northwind.app/r/q3-revenue-8k2f</span>
    <div class="pop-acts mt-3 flex items-center gap-2.5"><button class="inline-flex h-[30px] cursor-pointer items-center rounded-[9px] border border-[#0f766e] bg-[#0f766e] px-3 text-[.8125rem] leading-none font-semibold text-white no-underline transition-colors duration-150 hover:bg-[#0b5f58] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f766e]" type="button" data-copy="https://northwind.app/r/q3-revenue-8k2f">Copy link</button><p class="m-0 text-[.8125rem] font-semibold text-[#0f6b62]" role="status"></p></div>
    <button class="absolute top-2.5 right-2.5 grid size-[30px] cursor-pointer place-items-center rounded-lg border-0 bg-transparent p-0 text-[#56635f] hover:bg-[#eef3f2] hover:text-[#1b2b29] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#0f766e]" type="button" popovertarget="share-pop" popovertargetaction="hide" aria-label="Close"><svg class="size-4" aria-hidden="true"><use href="#i-x"/></svg></button>
  </div>
</div></div>
CSS
@theme {
  --color-tt: #14302c;
  --color-tt-ink: #f3faf8;
}
/* tt-open: a tooltip inside a .tt wrapper that is hovered, holds focus or was opened by tap, and was not dismissed with Esc. */
@custom-variant tt-open (&:is(:where(.tt):is(:hover, :focus-within, .is-open):not(.is-dismissed) > *));
/* tt-hover: hovered but not focused, so the hovercard waits 250 ms before it appears. */
@custom-variant tt-hover (&:is(:where(.tt):hover:not(:focus-within, .is-open, .is-dismissed) > *));
JavaScript
// Hovercards and tooltips (WCAG 1.4.13): Esc hides the open one and, if focus was inside a card, returns it to the trigger;
// a tap opens it on touch screens; opening one closes the others; cards and tooltips are nudged to stay inside the window.
(() => {
  const tts = [...document.querySelectorAll('.tt')];
  const fit = t => {
    const tip = t.querySelector(':scope > .tt-top, :scope > .tt-card'); if (!tip) return;
    tip.style.removeProperty('--tt-nudge');
    requestAnimationFrame(() => {
      const r = tip.getBoundingClientRect(), w = document.documentElement.clientWidth;
      const d = r.left < 8 ? 8 - r.left : r.right > w - 8 ? w - 8 - r.right : 0;
      if (d) tip.style.setProperty('--tt-nudge', d + 'px');
    });
  };
  const enter = t => { tts.forEach(o => o !== t && o.classList.remove('is-open')); t.classList.remove('is-dismissed'); fit(t); };
  tts.forEach(t => { t.addEventListener('pointerenter', () => enter(t)); t.addEventListener('focusin', () => enter(t)); if (t.classList.contains('is-open')) fit(t); });
  document.addEventListener('pointerdown', e => {
    const t = e.target.closest('.tt');
    if (!t) tts.forEach(o => o.classList.remove('is-open'));
    else if (e.pointerType === 'touch') t.classList.add('is-open');
  });
  document.addEventListener('keydown', e => {
    if (e.key !== 'Escape') return;
    tts.filter(t => t.matches(':hover, :focus-within, .is-open')).forEach(t => {
      const trigger = t.firstElementChild;
      if (t.contains(document.activeElement) && document.activeElement !== trigger) trigger.focus();
      t.classList.remove('is-open'); t.classList.add('is-dismissed');
    });
  });

  // Click popovers: place each one under its button (the popover attribute already handles Esc, outside clicks and focus).
  document.querySelectorAll('.pop[popover]').forEach(pop => {
    const btn = document.querySelector(`[popovertarget="${pop.id}"]`);
    const place = () => {
      const r = btn.getBoundingClientRect(), vw = document.documentElement.clientWidth, w = pop.offsetWidth;
      const left = Math.min(Math.max(8, r.left + r.width / 2 - w / 2), vw - w - 8);
      pop.style.left = left + 'px'; pop.style.top = r.bottom + 12 + 'px';
      pop.style.setProperty('--arrow-x', r.left + r.width / 2 - left + 'px');
    };
    pop.addEventListener('toggle', e => {
      const on = e.newState === 'open' ? addEventListener : removeEventListener;
      if (e.newState === 'open') place();
      on('scroll', place, true); on('resize', place);
    });
  });
  document.querySelectorAll('[data-copy]').forEach(b => b.addEventListener('click', () => {
    navigator.clipboard?.writeText(b.dataset.copy).catch(() => {});
    b.closest('.pop-acts').querySelector('[role="status"]').textContent = 'Link copied';
  }));
})();

About this set

Tooltips that carry more than a word, and a popover for content you can act on. Hovercard on a name shows a profile card for Mara Okafor in an activity feed, with her role, a short note, her local time and a Message link; it waits 250 milliseconds before opening on hover so the card does not flash as the pointer crosses the feed, but opens at once on keyboard focus, and Tab moves into it. Rich tooltip with a title explains two connection badges, Sync paused and Connected, with a bold heading and a sentence, and holds no controls. Popover that opens on click shares a Q3 revenue report: a heading, a sentence about who can see it, the link, a Copy link button that reports Link copied in a status message, and a close button. Use a rich tooltip for read-only detail, a hovercard for an optional shortcut, and a click popover whenever the content has buttons or links people need. The plain CSS and Tailwind popovers use the popover attribute, so Esc, outside clicks and returning focus are handled by the browser, with a few lines that place it under its button. The Bootstrap version uses Bootstrap Tooltip and Popover.

What’s included

  • Hovercard with a profile, local time and a Message link
  • Rich tooltip with a heading and a sentence
  • Click popover with a copy action and a status message
  • Hovercard waits 250 ms on hover and opens at once on focus
  • Esc closes and returns focus to the trigger
  • Bootstrap Tooltip and Popover in the Bootstrap version

Accessibility

The rich tooltip is linked with aria-describedby and has role="tooltip". The hovercard is reached with Tab from its link, and Esc closes it and moves focus back to the link. The click popover is a labelled dialog opened by a button with aria-expanded; Esc and an outside click close it and focus returns to the button, and the copy result is announced through role="status". Focus outlines are visible and motion stops under prefers-reduced-motion.

Customise it

Colours are --tt-bg and --tt-ink for the rich tooltip and plain hex values on .tt-card and .pop in plain CSS. In Bootstrap edit the --bs-tooltip-* variables on .tooltip.tt-rich, the --bs-popover-* variables on .popover.pop and the --bs-card-* variables on .tt-card. In Tailwind change the theme colours and the hex utilities.