Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.7 2.7L16.5 9.5"/></symbol>
  <symbol id="i-warn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4 2.8 19.5h18.4z"/><path d="M12 10v4M12 17v.01"/></symbol>
  <symbol id="i-bad" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="m9 9 6 6M15 9l-6 6"/></symbol>
  <symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8v.01"/></symbol>
  <symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 14a4 4 0 0 0 5.7 0l3.1-3.1a4 4 0 0 0-5.7-5.7L11.5 6.8"/><path d="M14 10a4 4 0 0 0-5.7 0l-3.1 3.1a4 4 0 0 0 5.7 5.7l1.6-1.6"/></symbol>
</svg>

<div><span class="lbl">Status colours</span>
<div class="stage grid4">
  <div class="cell"><span class="tt"><button class="chip ok" type="button" aria-describedby="tip-ok"><i aria-hidden="true"></i>api · Deployed</button><span class="tt-tip tt-top tt-success" role="tooltip" id="tip-ok"><svg aria-hidden="true"><use href="#i-ok"/></svg>Live since 09:42, build 1.8.3</span></span></div>
  <div class="cell"><span class="tt"><button class="chip warn" type="button" aria-describedby="tip-warn"><i aria-hidden="true"></i>search · Slow</button><span class="tt-tip tt-top tt-warning" role="tooltip" id="tip-warn"><svg aria-hidden="true"><use href="#i-warn"/></svg>p95 latency 1.4 s, normally 220 ms</span></span></div>
  <div class="cell"><span class="tt"><button class="chip bad" type="button" aria-describedby="tip-bad"><i aria-hidden="true"></i>web · Failed</button><span class="tt-tip tt-top tt-danger" role="tooltip" id="tip-bad"><svg aria-hidden="true"><use href="#i-bad"/></svg>Tests failed in step 3 of 5</span></span></div>
  <div class="cell"><span class="tt"><button class="chip info" type="button" aria-describedby="tip-info"><i aria-hidden="true"></i>jobs · Queued</button><span class="tt-tip tt-top tt-info" role="tooltip" id="tip-info"><svg aria-hidden="true"><use href="#i-info"/></svg>2 deploys ahead, about 4 minutes</span></span></div>
</div></div>

<div><span class="lbl">Light and dark</span>
<div class="stage duo">
  <div class="cell dark"><span class="tt"><button class="btn on-dark" type="button" aria-describedby="tip-light"><svg aria-hidden="true"><use href="#i-link"/></svg>Copy invite link</button><span class="tt-tip tt-top tt-light" role="tooltip" id="tip-light">Anyone with the link can join until Friday</span></span></div>
  <div class="cell"><span class="tt"><button class="btn" type="button" aria-describedby="tip-dark"><svg aria-hidden="true"><use href="#i-link"/></svg>Copy invite link</button><span class="tt-tip tt-top tt-dark" role="tooltip" id="tip-dark">Anyone with the link can join until Friday</span></span></div>
</div></div>

<div><span class="lbl">Three sizes</span>
<div class="stage">
  <div class="sizes">
    <span class="tt"><button class="av" type="button" aria-label="Ana Lima" aria-describedby="tip-sm">AL</button><span class="tt-tip tt-top tt-sm" role="tooltip" id="tip-sm">Ana Lima, editor</span></span>
    <span class="tt"><button class="btn" type="button" aria-describedby="tip-md">Export CSV</button><span class="tt-tip tt-top" role="tooltip" id="tip-md">2,418 rows, filters applied</span></span>
    <span class="tt"><button class="badge" type="button" aria-describedby="tip-lg">Pro trial</button><span class="tt-tip tt-top tt-lg" role="tooltip" id="tip-lg">9 days left. Your projects stay as they are if you do not upgrade.</span></span>
  </div>
</div></div>
CSS
/* Tooltip core. Themes only change --tt-bg and --tt-ink on the tooltip; sizes change its padding and type. */
.tt{--tt-gap:10px;--tt-arrow:6px;position:relative;display:inline-flex;vertical-align:middle}
.tt-tip{--tt-bg:#1c1917;--tt-ink:#fafaf9;position:absolute;z-index:30;display:flex;align-items:flex-start;gap:7px;width:max-content;max-width:var(--tt-max,220px);padding:7px 11px;border-radius:8px;
  background:var(--tt-bg);color:var(--tt-ink);font:500 .8125rem/1.45 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-align:left;white-space:normal;
  box-shadow:0 10px 24px -10px rgba(28,25,23,.55);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-tip{visibility:visible;opacity:1;scale:1;transition:opacity .15s,visibility 0s,scale 0s}
.tt-tip::before{content:"";position:absolute;border:var(--tt-arrow) solid transparent}
.tt-tip::after{content:"";position:absolute}
.tt-tip svg{flex:none;width:15px;height:15px;margin-top:2px}
.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)}

/* Themes */
.tt-success{--tt-bg:#047857;--tt-ink:#fff}
.tt-warning{--tt-bg:#fcd34d;--tt-ink:#422006}
.tt-danger{--tt-bg:#b91c1c;--tt-ink:#fff}
.tt-info{--tt-bg:#1d4ed8;--tt-ink:#fff}
.tt-light{--tt-bg:#fff;--tt-ink:#292524;box-shadow:none;filter:drop-shadow(0 0 .6px #78716c) drop-shadow(0 8px 14px rgba(28,25,23,.16))}
.tt-dark{--tt-bg:#1c1917;--tt-ink:#fafaf9}
/* Sizes */
.tt-sm{padding:4px 8px;border-radius:6px;font-size:.75rem;--tt-arrow:5px}
.tt-lg{--tt-max:280px;padding:11px 14px;border-radius:10px;font-size:.875rem;line-height:1.5}

/* Triggers */
.chip{display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 12px;border:1px solid;border-radius:999px;background:#fff;font:600 .8125rem/1 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;cursor:help;white-space:nowrap}
.chip i{width:8px;height:8px;border-radius:50%;background:currentColor}
.chip.ok{border-color:#a7dcc6;background:#ecfdf5;color:#065f46}
.chip.warn{border-color:#f2d58a;background:#fffbeb;color:#854d0e}
.chip.bad{border-color:#f3b4b4;background:#fef2f2;color:#991b1b}
.chip.info{border-color:#b8c9f5;background:#eff4ff;color:#1e40af}
.btn{display:inline-flex;align-items:center;gap:8px;height:38px;padding:0 15px;border:1px solid #d6d3d1;border-radius:10px;background:#fff;color:#1c1917;font:600 .875rem/1 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;cursor:pointer}
.btn:hover{background:#f5f5f4}
.btn.on-dark{border-color:#44403c;background:#292524;color:#fafaf9}
.btn.on-dark:hover{background:#3a3532}
.btn svg{width:16px;height:16px}
.av{display:grid;place-items:center;width:28px;height:28px;padding:0;border:2px solid #fff;border-radius:50%;background:#fed7aa;color:#7c2d12;font:700 .6875rem/1 ui-sans-serif,system-ui,sans-serif;cursor:pointer}
.badge{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 11px;border:0;border-radius:8px;background:#1c1917;color:#fde68a;font:700 .75rem/1 ui-sans-serif,system-ui,sans-serif;letter-spacing:.04em;text-transform:uppercase;cursor:help}
:is(.chip,.btn,.av,.badge):focus-visible{outline:2px solid #ea580c;outline-offset:2px}
.btn.on-dark:focus-visible{outline-color:#fdba74}

/* Demo layout */
.grid4{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}
@media (max-width:600px){.grid4{grid-template-columns:1fr}}
.cell{display:flex;align-items:flex-end;justify-content:center;min-height:118px;padding:14px;border:1px dashed #d6d3d1;border-radius:12px;background:#fafaf9}
.cell.dark{border:0;background:#1c1917}
.duo{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}
.duo .cell{min-height:130px}
.sizes{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));align-items:end;justify-items:center;gap:92px 24px;padding:84px 0 4px}
@media (prefers-reduced-motion:reduce){.tt .tt-tip,.tt:is(:hover,:focus-within,.is-open):not(.is-dismissed)>.tt-tip{transition:none}}
JavaScript
// Tooltip behaviour for WCAG 1.4.13: Esc dismisses without moving focus or the pointer, a tap opens it on touch screens,
// opening one closes the others, and tooltips are nudged sideways to stay inside the window.
(() => {
  const tts = [...document.querySelectorAll('.tt')];
  const fit = t => {
    const tip = t.querySelector(':scope > [role="tooltip"]'); 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 => { t.classList.remove('is-open'); t.classList.add('is-dismissed'); });
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div><span class="lbl">Status colours</span>
<div class="stage">
  <div class="row row-cols-1 row-cols-sm-2 g-3">
    <div class="col"><div class="cell"><button class="chip ok rounded-pill" type="button" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" data-bs-custom-class="tt-success"
      data-bs-title='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.7 2.7L16.5 9.5"/></svg>Live since 09:42, build 1.8.3'><i aria-hidden="true"></i>api · Deployed</button></div></div>
    <div class="col"><div class="cell"><button class="chip warn rounded-pill" type="button" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" data-bs-custom-class="tt-warning"
      data-bs-title='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4 2.8 19.5h18.4z"/><path d="M12 10v4M12 17v.01"/></svg>p95 latency 1.4 s, normally 220 ms'><i aria-hidden="true"></i>search · Slow</button></div></div>
    <div class="col"><div class="cell"><button class="chip bad rounded-pill" type="button" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" data-bs-custom-class="tt-danger"
      data-bs-title='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="m9 9 6 6M15 9l-6 6"/></svg>Tests failed in step 3 of 5'><i aria-hidden="true"></i>web · Failed</button></div></div>
    <div class="col"><div class="cell"><button class="chip info rounded-pill" type="button" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" data-bs-custom-class="tt-info"
      data-bs-title='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8v.01"/></svg>2 deploys ahead, about 4 minutes'><i aria-hidden="true"></i>jobs · Queued</button></div></div>
  </div>
</div></div>

<div><span class="lbl">Light and dark</span>
<div class="stage duo">
  <div class="row row-cols-1 row-cols-sm-2 g-3">
    <div class="col"><div class="cell dark"><button class="btn btn-stone on-dark" type="button" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-custom-class="tt-light" data-bs-title="Anyone with the link can join until Friday"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.7 0l3.1-3.1a4 4 0 0 0-5.7-5.7L11.5 6.8"/><path d="M14 10a4 4 0 0 0-5.7 0l-3.1 3.1a4 4 0 0 0 5.7 5.7l1.6-1.6"/></svg>Copy invite link</button></div></div>
    <div class="col"><div class="cell"><button class="btn btn-stone" type="button" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="Anyone with the link can join until Friday"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.7 0l3.1-3.1a4 4 0 0 0-5.7-5.7L11.5 6.8"/><path d="M14 10a4 4 0 0 0-5.7 0l-3.1 3.1a4 4 0 0 0 5.7 5.7l1.6-1.6"/></svg>Copy invite link</button></div></div>
  </div>
</div></div>

<div><span class="lbl">Three sizes</span>
<div class="stage">
  <div class="sizes">
    <button class="av rounded-circle" type="button" aria-label="Ana Lima" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-custom-class="tt-sm" data-bs-title="Ana Lima, editor">AL</button>
    <button class="btn btn-stone" type="button" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="2,418 rows, filters applied">Export CSV</button>
    <button class="trial" type="button" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-custom-class="tt-lg" data-bs-title="9 days left. Your projects stay as they are if you do not upgrade.">Pro trial</button>
  </div>
</div></div>
CSS
/* Bootstrap Tooltip themed with a custom class per colour and size. Each class only sets Bootstrap's --bs-tooltip-* variables. */
.tooltip.tt{--bs-tooltip-bg:#1c1917;--bs-tooltip-color:#fafaf9;--bs-tooltip-opacity:1;--bs-tooltip-max-width:220px;--bs-tooltip-padding-x:11px;--bs-tooltip-padding-y:7px;
  --bs-tooltip-font-size:.8125rem;--bs-tooltip-border-radius:8px;--bs-tooltip-arrow-width:12px;--bs-tooltip-arrow-height:6px;font-family:inherit}
.tooltip.tt .tooltip-inner{display:flex;align-items:flex-start;gap:7px;text-align:left;font-weight:500;line-height:1.45;box-shadow:0 10px 24px -10px rgba(28,25,23,.55)}
.tooltip.tt svg{flex:none;width:15px;height:15px;margin-top:2px}
.tooltip.tt-success{--bs-tooltip-bg:#047857;--bs-tooltip-color:#fff}
.tooltip.tt-warning{--bs-tooltip-bg:#fcd34d;--bs-tooltip-color:#422006}
.tooltip.tt-danger{--bs-tooltip-bg:#b91c1c;--bs-tooltip-color:#fff}
.tooltip.tt-info{--bs-tooltip-bg:#1d4ed8;--bs-tooltip-color:#fff}
.tooltip.tt-light{--bs-tooltip-bg:#fff;--bs-tooltip-color:#292524;filter:drop-shadow(0 0 .6px #78716c) drop-shadow(0 8px 14px rgba(28,25,23,.16))}
.tooltip.tt-light .tooltip-inner{box-shadow:none}
.tooltip.tt-sm{--bs-tooltip-padding-x:8px;--bs-tooltip-padding-y:4px;--bs-tooltip-font-size:.75rem;--bs-tooltip-border-radius:6px;--bs-tooltip-arrow-width:10px;--bs-tooltip-arrow-height:5px}
.tooltip.tt-lg{--bs-tooltip-max-width:280px;--bs-tooltip-padding-x:14px;--bs-tooltip-padding-y:11px;--bs-tooltip-font-size:.875rem;--bs-tooltip-border-radius:10px}
.tooltip.tt-lg .tooltip-inner{line-height:1.5}
.tooltip.show{animation:tt-in .15s ease-out}
@keyframes tt-in{from{opacity:0}}

/* Triggers */
.chip{display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 12px;border:1px solid;background:#fff;font-size:.8125rem;font-weight:600;line-height:1;white-space:nowrap;cursor:help}
.chip i{width:8px;height:8px;border-radius:50%;background:currentColor}
.chip.ok{border-color:#a7dcc6;background:#ecfdf5;color:#065f46}
.chip.warn{border-color:#f2d58a;background:#fffbeb;color:#854d0e}
.chip.bad{border-color:#f3b4b4;background:#fef2f2;color:#991b1b}
.chip.info{border-color:#b8c9f5;background:#eff4ff;color:#1e40af}
.btn-stone{--bs-btn-padding-x:15px;--bs-btn-font-size:.875rem;--bs-btn-font-weight:600;--bs-btn-line-height:1;--bs-btn-border-radius:10px;--bs-btn-color:#1c1917;--bs-btn-bg:#fff;
  --bs-btn-border-color:#d6d3d1;--bs-btn-hover-color:#1c1917;--bs-btn-hover-bg:#f5f5f4;--bs-btn-hover-border-color:#d6d3d1;--bs-btn-active-bg:#e7e5e4;--bs-btn-active-border-color:#d6d3d1;
  display:inline-flex;align-items:center;gap:8px;height:38px}
.btn-stone.on-dark{--bs-btn-color:#fafaf9;--bs-btn-bg:#292524;--bs-btn-border-color:#44403c;--bs-btn-hover-color:#fafaf9;--bs-btn-hover-bg:#3a3532;--bs-btn-hover-border-color:#44403c;
  --bs-btn-active-color:#fafaf9;--bs-btn-active-bg:#3a3532;--bs-btn-active-border-color:#44403c}
.btn-stone svg{width:16px;height:16px}
.av{display:grid;place-items:center;width:28px;height:28px;padding:0;border:2px solid #fff;background:#fed7aa;color:#7c2d12;font-size:.6875rem;font-weight:700;line-height:1}
.trial{display:inline-flex;align-items:center;height:28px;padding:0 11px;border:0;border-radius:8px;background:#1c1917;color:#fde68a;font-size:.75rem;font-weight:700;line-height:1;letter-spacing:.04em;text-transform:uppercase;cursor:help}
:is(.chip,.btn-stone,.av,.trial):focus-visible{outline:2px solid #ea580c;outline-offset:2px;box-shadow:none}
.btn-stone.on-dark:focus-visible{outline-color:#fdba74}

.cell{display:flex;align-items:flex-end;justify-content:center;min-height:118px;padding:14px;border:1px dashed #d6d3d1;border-radius:12px;background:#fafaf9}
.cell.dark{border:0;background:#1c1917}
.duo .cell{min-height:130px}
.sizes{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));align-items:end;justify-items:center;gap:92px 24px;padding:84px 0 4px}
@media (prefers-reduced-motion:reduce){.tooltip.show{animation:none}.btn{transition:none}}
JavaScript
// Bootstrap Tooltip (animation off, CSS fades it in), triggered by this script so it meets WCAG 1.4.13: it opens on hover,
// keyboard focus and tap, stays open while the pointer is on it, Esc closes it, and only one is open at a time.
(() => {
  const allowList = { ...bootstrap.Tooltip.Default.allowList, svg: ['viewbox', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'aria-hidden'], path: ['d'], circle: ['cx', 'cy', 'r'] };
  const tips = new Map();
  const hideAll = 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], allowList,
      customClass: ('tt ' + (el.dataset.bsCustomClass || '')).trim() });
    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); hideAll(tip); 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(); });
  });
  document.addEventListener('pointerdown', e => { if (![...tips.keys()].some(el => el.contains(e.target)) && !e.target.closest('.tooltip')) hideAll(); });
  document.addEventListener('keydown', e => { if (e.key === 'Escape') hideAll(); });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.7 2.7L16.5 9.5"/></symbol>
  <symbol id="i-warn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4 2.8 19.5h18.4z"/><path d="M12 10v4M12 17v.01"/></symbol>
  <symbol id="i-bad" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="m9 9 6 6M15 9l-6 6"/></symbol>
  <symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8v.01"/></symbol>
  <symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 14a4 4 0 0 0 5.7 0l3.1-3.1a4 4 0 0 0-5.7-5.7L11.5 6.8"/><path d="M14 10a4 4 0 0 0-5.7 0l-3.1 3.1a4 4 0 0 0 5.7 5.7l1.6-1.6"/></symbol>
</svg>

<div><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#57534e] uppercase">Status colours</span>
<div class="rounded-[18px] border border-[#e7e5e4] bg-white p-7 max-[600px]:p-5 grid grid-cols-2 gap-3 max-[600px]:grid-cols-1">
  <div class="flex min-h-[118px] items-end justify-center rounded-xl border border-dashed border-[#d6d3d1] bg-[#fafaf9] p-3.5"><span class="tt relative inline-flex align-middle"><button class="inline-flex h-8 cursor-help items-center gap-[7px] rounded-full border px-3 text-[.8125rem] leading-none font-semibold whitespace-nowrap focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ea580c] border-[#a7dcc6] bg-[#ecfdf5] text-[#065f46]" type="button" aria-describedby="tip-ok"><i class="size-2 rounded-full bg-current" aria-hidden="true"></i>api · Deployed</button><span class="pointer-events-auto absolute z-30 w-max max-w-[220px] rounded-lg bg-tt-success px-[11px] py-[7px] text-[.8125rem] leading-[1.45] font-medium text-white whitespace-normal shadow-[0_10px_24px_-10px_rgba(28,25,23,.55)] 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-success after:inset-x-0 after:top-full after:h-[10px] flex items-start gap-[7px] text-left" role="tooltip" id="tip-ok"><svg class="mt-0.5 size-[15px] flex-none" aria-hidden="true"><use href="#i-ok"/></svg>Live since 09:42, build 1.8.3</span></span></div>
  <div class="flex min-h-[118px] items-end justify-center rounded-xl border border-dashed border-[#d6d3d1] bg-[#fafaf9] p-3.5"><span class="tt relative inline-flex align-middle"><button class="inline-flex h-8 cursor-help items-center gap-[7px] rounded-full border px-3 text-[.8125rem] leading-none font-semibold whitespace-nowrap focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ea580c] border-[#f2d58a] bg-[#fffbeb] text-[#854d0e]" type="button" aria-describedby="tip-warn"><i class="size-2 rounded-full bg-current" aria-hidden="true"></i>search · Slow</button><span class="pointer-events-auto absolute z-30 w-max max-w-[220px] rounded-lg bg-tt-warning px-[11px] py-[7px] text-[.8125rem] leading-[1.45] font-medium text-tt-warning-ink whitespace-normal shadow-[0_10px_24px_-10px_rgba(28,25,23,.55)] 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-warning after:inset-x-0 after:top-full after:h-[10px] flex items-start gap-[7px] text-left" role="tooltip" id="tip-warn"><svg class="mt-0.5 size-[15px] flex-none" aria-hidden="true"><use href="#i-warn"/></svg>p95 latency 1.4 s, normally 220 ms</span></span></div>
  <div class="flex min-h-[118px] items-end justify-center rounded-xl border border-dashed border-[#d6d3d1] bg-[#fafaf9] p-3.5"><span class="tt relative inline-flex align-middle"><button class="inline-flex h-8 cursor-help items-center gap-[7px] rounded-full border px-3 text-[.8125rem] leading-none font-semibold whitespace-nowrap focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ea580c] border-[#f3b4b4] bg-[#fef2f2] text-[#991b1b]" type="button" aria-describedby="tip-bad"><i class="size-2 rounded-full bg-current" aria-hidden="true"></i>web · Failed</button><span class="pointer-events-auto absolute z-30 w-max max-w-[220px] rounded-lg bg-tt-danger px-[11px] py-[7px] text-[.8125rem] leading-[1.45] font-medium text-white whitespace-normal shadow-[0_10px_24px_-10px_rgba(28,25,23,.55)] 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-danger after:inset-x-0 after:top-full after:h-[10px] flex items-start gap-[7px] text-left" role="tooltip" id="tip-bad"><svg class="mt-0.5 size-[15px] flex-none" aria-hidden="true"><use href="#i-bad"/></svg>Tests failed in step 3 of 5</span></span></div>
  <div class="flex min-h-[118px] items-end justify-center rounded-xl border border-dashed border-[#d6d3d1] bg-[#fafaf9] p-3.5"><span class="tt relative inline-flex align-middle"><button class="inline-flex h-8 cursor-help items-center gap-[7px] rounded-full border px-3 text-[.8125rem] leading-none font-semibold whitespace-nowrap focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ea580c] border-[#b8c9f5] bg-[#eff4ff] text-[#1e40af]" type="button" aria-describedby="tip-info"><i class="size-2 rounded-full bg-current" aria-hidden="true"></i>jobs · Queued</button><span class="pointer-events-auto absolute z-30 w-max max-w-[220px] rounded-lg bg-tt-info px-[11px] py-[7px] text-[.8125rem] leading-[1.45] font-medium text-white whitespace-normal shadow-[0_10px_24px_-10px_rgba(28,25,23,.55)] 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-info after:inset-x-0 after:top-full after:h-[10px] flex items-start gap-[7px] text-left" role="tooltip" id="tip-info"><svg class="mt-0.5 size-[15px] flex-none" aria-hidden="true"><use href="#i-info"/></svg>2 deploys ahead, about 4 minutes</span></span></div>
</div></div>

<div><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#57534e] uppercase">Light and dark</span>
<div class="rounded-[18px] border border-[#e7e5e4] bg-white p-7 max-[600px]:p-5 grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-3">
  <div class="flex min-h-[130px] items-end justify-center rounded-xl bg-[#1c1917] p-3.5"><span class="tt relative inline-flex align-middle"><button class="inline-flex h-[38px] cursor-pointer items-center gap-2 rounded-[10px] border px-[15px] text-sm leading-none font-semibold border-[#44403c] bg-[#292524] text-[#fafaf9] hover:bg-[#3a3532] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#fdba74]" type="button" aria-describedby="tip-light"><svg class="size-4" aria-hidden="true"><use href="#i-link"/></svg>Copy invite link</button><span class="pointer-events-auto absolute z-30 w-max max-w-[220px] rounded-lg bg-white px-[11px] py-[7px] text-[.8125rem] leading-[1.45] font-medium text-tt-light-ink whitespace-normal [filter:drop-shadow(0_0_.6px_#78716c)_drop-shadow(0_8px_14px_rgba(28,25,23,.16))] 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-white after:inset-x-0 after:top-full after:h-[10px] flex items-start gap-[7px] text-left" role="tooltip" id="tip-light">Anyone with the link can join until Friday</span></span></div>
  <div class="flex min-h-[118px] items-end justify-center rounded-xl border border-dashed border-[#d6d3d1] bg-[#fafaf9] p-3.5 min-h-[130px]"><span class="tt relative inline-flex align-middle"><button class="inline-flex h-[38px] cursor-pointer items-center gap-2 rounded-[10px] border px-[15px] text-sm leading-none font-semibold border-[#d6d3d1] bg-white text-[#1c1917] hover:bg-[#f5f5f4] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ea580c]" type="button" aria-describedby="tip-dark"><svg class="size-4" aria-hidden="true"><use href="#i-link"/></svg>Copy invite link</button><span class="pointer-events-auto absolute z-30 w-max max-w-[220px] rounded-lg bg-tt px-[11px] py-[7px] text-[.8125rem] leading-[1.45] font-medium text-tt-ink whitespace-normal shadow-[0_10px_24px_-10px_rgba(28,25,23,.55)] 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] flex items-start gap-[7px] text-left" role="tooltip" id="tip-dark">Anyone with the link can join until Friday</span></span></div>
</div></div>

<div><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#57534e] uppercase">Three sizes</span>
<div class="rounded-[18px] border border-[#e7e5e4] bg-white p-7 max-[600px]:p-5">
  <div class="grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] items-end justify-items-center gap-x-6 gap-y-[92px] pt-[84px] pb-1">
    <span class="tt relative inline-flex align-middle"><button class="grid size-7 cursor-pointer place-items-center rounded-full border-2 border-white bg-[#fed7aa] p-0 text-[.6875rem] leading-none font-bold text-[#7c2d12] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ea580c]" type="button" aria-label="Ana Lima" aria-describedby="tip-sm">AL</button><span class="pointer-events-auto absolute z-30 w-max max-w-[220px] rounded-md bg-tt px-2 py-1 text-xs leading-[1.45] font-medium text-tt-ink whitespace-normal shadow-[0_10px_24px_-10px_rgba(28,25,23,.55)] before:absolute before:border-transparent after:absolute before:border-[5px] 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%-5px-var(--tt-nudge,0px))] before:border-b-0 before:border-t-tt after:inset-x-0 after:top-full after:h-[10px] flex items-start gap-[7px] text-left" role="tooltip" id="tip-sm">Ana Lima, editor</span></span>
    <span class="tt relative inline-flex align-middle"><button class="inline-flex h-[38px] cursor-pointer items-center gap-2 rounded-[10px] border px-[15px] text-sm leading-none font-semibold border-[#d6d3d1] bg-white text-[#1c1917] hover:bg-[#f5f5f4] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ea580c]" type="button" aria-describedby="tip-md">Export CSV</button><span class="pointer-events-auto absolute z-30 w-max max-w-[220px] rounded-lg bg-tt px-[11px] py-[7px] text-[.8125rem] leading-[1.45] font-medium text-tt-ink whitespace-normal shadow-[0_10px_24px_-10px_rgba(28,25,23,.55)] 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] flex items-start gap-[7px] text-left" role="tooltip" id="tip-md">2,418 rows, filters applied</span></span>
    <span class="tt relative inline-flex align-middle"><button class="inline-flex h-7 cursor-help items-center rounded-lg border-0 bg-[#1c1917] px-[11px] text-xs leading-none font-bold tracking-[.04em] text-[#fde68a] uppercase focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ea580c]" type="button" aria-describedby="tip-lg">Pro trial</button><span class="pointer-events-auto absolute z-30 w-max max-w-[280px] rounded-[10px] bg-tt px-3.5 py-[11px] text-sm leading-[1.45] font-medium text-tt-ink whitespace-normal shadow-[0_10px_24px_-10px_rgba(28,25,23,.55)] 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] flex items-start gap-[7px] text-left" role="tooltip" id="tip-lg">9 days left. Your projects stay as they are if you do not upgrade.</span></span>
  </div>
</div></div>
CSS
@theme {
  --color-tt: #1c1917;
  --color-tt-ink: #fafaf9;
  --color-tt-success: #047857;
  --color-tt-warning: #fcd34d;
  --color-tt-warning-ink: #422006;
  --color-tt-danger: #b91c1c;
  --color-tt-info: #1d4ed8;
  --color-tt-light-ink: #292524;
}
/* 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) > *));
JavaScript
// Tooltip behaviour for WCAG 1.4.13: Esc dismisses without moving focus or the pointer, a tap opens it on touch screens,
// opening one closes the others, and tooltips are nudged sideways to stay inside the window.
(() => {
  const tts = [...document.querySelectorAll('.tt')];
  const fit = t => {
    const tip = t.querySelector(':scope > [role="tooltip"]'); 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 => { t.classList.remove('is-open'); t.classList.add('is-dismissed'); });
  });
})();

About this set

One tooltip in the colours and sizes a design system needs. Status colours shows four service badges from a deploy dashboard, api Deployed, search Slow, web Failed and jobs Queued, each with a tooltip in success green, warning amber, danger red or info blue, an icon that matches, and a specific detail such as the p95 latency or the failed build step. Light and dark shows the same Copy invite link tooltip as a white tooltip on a dark panel and a near-black one on a light panel; the white one is outlined with filter: drop-shadow, which follows the arrow as well as the bubble. Three sizes puts a small tooltip on an avatar, a medium one on an Export CSV button and a large two-line one on a Pro trial badge. Every theme only changes two custom properties, –tt-bg and –tt-ink, and the arrow takes its colour from the same variable, so adding a brand colour is one line. All colour pairs meet 4.5:1 contrast, including dark text on the amber warning. The Bootstrap version passes each theme as a custom class to Bootstrap Tooltip.

What’s included

  • Success, warning, danger and info colours with icons
  • Light tooltip outlined with filter: drop-shadow, including the arrow
  • Small, medium and large sizes
  • Each theme changes only --tt-bg and --tt-ink
  • All text and background pairs reach 4.5:1
  • Bootstrap themes through customClass

Accessibility

Every badge and button is linked to its tooltip with aria-describedby, and the colour is never the only signal: each status says its state in words on the badge and in the tooltip. Tooltips open on hover and keyboard focus, Esc hides them and they stay open under the pointer. Focus outlines are visible, and the fade is removed under prefers-reduced-motion.

Customise it

Add a class that sets --tt-bg and --tt-ink, for example .tt-brand{--tt-bg:#7c3aed;--tt-ink:#fff}, and sizes through padding and font-size on .tt-sm and .tt-lg. In Bootstrap set --bs-tooltip-bg and --bs-tooltip-color on a .tooltip.tt-* class, and in Tailwind add a --color-tt-* theme colour.