Open demo

CSS code

HTML
<div><span class="lbl">Four directions</span>
<div class="stage dirs">
  <div class="cell"><span class="tt"><button class="tb" type="button" aria-describedby="tt-dup">Duplicate</button><span class="tt-tip tt-top" role="tooltip" id="tt-dup">Copies the page and its comments</span></span></div>
  <div class="cell to-right"><span class="tt"><button class="tb" type="button" aria-describedby="tt-move">Move</button><span class="tt-tip tt-right" role="tooltip" id="tt-move">Pick another folder</span></span></div>
  <div class="cell"><span class="tt"><button class="tb" type="button" aria-describedby="tt-export">Export</button><span class="tt-tip tt-bottom" role="tooltip" id="tt-export">PDF, PNG or SVG</span></span></div>
  <div class="cell to-left"><span class="tt"><button class="tb" type="button" aria-describedby="tt-archive">Archive</button><span class="tt-tip tt-left" role="tooltip" id="tt-archive">Restorable for 30 days</span></span></div>
</div></div>

<div><span class="lbl">Definitions inside text</span>
<div class="stage">
  <p class="prose">A <span class="tt"><button class="term" type="button" aria-describedby="tt-reserve">rolling reserve</button><span class="tt-tip tt-top tt-start" role="tooltip" id="tt-reserve">A share of each payout held back and released automatically after 90 days.</span></span> of 5% is held from every payout to cover <span class="tt"><button class="term" type="button" aria-describedby="tt-chargeback">chargebacks</button><span class="tt-tip tt-top tt-start" role="tooltip" id="tt-chargeback">A refund your customer's bank forces after a dispute, plus a $15 fee.</span></span>. Everything else reaches your bank two business days after the sale.</p>
</div></div>

<div><span class="lbl">Explaining a disabled action</span>
<div class="stage">
  <div class="post">
    <div><b>Five rituals of slow mornings</b><small><i aria-hidden="true"></i>Draft · cover image missing</small></div>
    <div class="acts">
      <span class="tt"><button class="tb" type="button" aria-describedby="tt-save">Save draft</button><span class="tt-tip tt-bottom" role="tooltip" id="tt-save">Last saved 2 minutes ago</span></span>
      <span class="tt"><button class="tb primary" type="button" aria-disabled="true" aria-describedby="tt-publish">Publish</button><span class="tt-tip tt-bottom" role="tooltip" id="tt-publish">Add a cover image before publishing</span></span>
    </div>
  </div>
</div></div>
CSS
/* Tooltips: a wrapper (.tt) holds the trigger and its tooltip. CSS shows the tooltip on hover and keyboard focus;
   the script adds Esc to dismiss, tap to open on touch screens and a nudge that keeps it inside the window. */
.tt{--tt-bg:#1b2032;--tt-ink:#f8fafc;--tt-gap:10px;--tt-arrow:6px;--tt-max:240px;position:relative;display:inline-flex;vertical-align:middle}
.tt-tip{position:absolute;z-index:30;width:max-content;max-width:var(--tt-max);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:center;white-space:normal;
  box-shadow:0 10px 24px -10px rgba(15,23,42,.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} /* fills the gap so the pointer can move onto the tooltip */
.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)}
.tt-bottom{top:calc(100% + var(--tt-gap));left:50%;translate:calc(-50% + var(--tt-nudge,0px)) 0}
.tt-bottom::before{bottom:100%;left:calc(50% - var(--tt-arrow) - var(--tt-nudge,0px));border-bottom-color:var(--tt-bg);border-top-width:0}
.tt-bottom::after{bottom:100%;left:0;right:0;height:var(--tt-gap)}
.tt-right{left:calc(100% + var(--tt-gap));top:50%;translate:0 -50%}
.tt-right::before{right:100%;top:calc(50% - var(--tt-arrow));border-right-color:var(--tt-bg);border-left-width:0}
.tt-right::after{right:100%;top:0;bottom:0;width:var(--tt-gap)}
.tt-left{right:calc(100% + var(--tt-gap));top:50%;translate:0 -50%}
.tt-left::before{left:100%;top:calc(50% - var(--tt-arrow));border-left-color:var(--tt-bg);border-right-width:0}
.tt-left::after{left:100%;top:0;bottom:0;width:var(--tt-gap)}
.tt-tip.tt-start{left:0;translate:var(--tt-nudge,0px) 0;text-align:left}
.tt-tip.tt-start::before{left:calc(14px - var(--tt-nudge,0px))}

/* Demo triggers */
.tb{display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 16px;border:1px solid #cdd3de;border-radius:10px;background:#fff;color:#1f2433;
  font:600 .875rem/1 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;cursor:pointer;transition:background-color .15s,border-color .15s}
.tb:hover{background:#f5f6f9;border-color:#b9c1cf}
.tb:focus-visible,.term:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}
.tb.primary{background:#4f46e5;border-color:#4f46e5;color:#fff}
.tb.primary:hover{background:#4338ca}
.tb[aria-disabled="true"]{background:#eef0f4;border-color:#dfe3ea;color:#5f6878;cursor:not-allowed}
.tb svg{width:16px;height:16px}
.dirs{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}
@media (max-width:600px){.dirs{grid-template-columns:1fr}}
.cell{display:flex;align-items:center;justify-content:center;height:140px;padding:0 20px;border:1px dashed #d6dbe4;border-radius:12px;background:#f8f9fb}
.cell.to-right{justify-content:flex-start}
.cell.to-left{justify-content:flex-end}
.term{padding:0;border:0;background:none;color:#3730a3;font:inherit;font-weight:600;text-decoration:underline dotted 1.5px;text-underline-offset:4px;cursor:help;border-radius:3px}
.prose{max-width:560px;margin:0;padding-top:58px;font-size:1rem;line-height:1.75;color:#343a4a}
.post{margin-bottom:48px;display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding:16px;border:1px solid #e1e4eb;border-radius:14px;background:#fafbfc}
.post b{display:block;font-size:.95rem}
.post small{display:flex;align-items:center;gap:6px;color:#5b6474;font-size:.8125rem}
.post small i{width:8px;height:8px;border-radius:50%;background:#d97706}
.post .acts{display:flex;gap:10px;margin-left:auto;padding-bottom:4px}
@media (prefers-reduced-motion:reduce){.tt .tt-tip,.tt:is(:hover,:focus-within,.is-open):not(.is-dismissed)>.tt-tip,.tb{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 top/bottom tooltips are nudged sideways to stay inside the window.
(() => {
  const tts = [...document.querySelectorAll('.tt')];
  const fit = t => {
    const tip = t.querySelector(':scope > .tt-top, :scope > .tt-bottom'); 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">Four directions</span>
<div class="stage dirs">
  <div class="cell"><button class="btn tb" type="button" id="tt-dup" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="Copies the page and its comments">Duplicate</button></div>
  <div class="cell to-right"><button class="btn tb" type="button" id="tt-move" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title="Pick another folder">Move</button></div>
  <div class="cell"><button class="btn tb" type="button" id="tt-export" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="PDF, PNG or SVG">Export</button></div>
  <div class="cell to-left"><button class="btn tb" type="button" id="tt-archive" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="Restorable for 30 days">Archive</button></div>
</div></div>

<div><span class="lbl">Definitions inside text</span>
<div class="stage">
  <p class="prose">A <button class="term" type="button" id="tt-reserve" data-bs-toggle="tooltip" data-bs-placement="top" data-tt-align="start" data-bs-custom-class="tt-start" data-bs-title="A share of each payout held back and released automatically after 90 days.">rolling reserve</button> of 5% is held from every payout to cover <button class="term" type="button" data-bs-toggle="tooltip" data-bs-placement="top" data-tt-align="start" data-bs-custom-class="tt-start" data-bs-title="A refund your customer's bank forces after a dispute, plus a $15 fee.">chargebacks</button>. Everything else reaches your bank two business days after the sale.</p>
</div></div>

<div><span class="lbl">Explaining a disabled action</span>
<div class="stage">
  <div class="post d-flex flex-wrap align-items-center gap-3">
    <div><b class="d-block" style="font-size:.95rem">Five rituals of slow mornings</b><small class="d-flex align-items-center gap-2" style="color:#5b6474;font-size:.8125rem"><i aria-hidden="true"></i>Draft · cover image missing</small></div>
    <div class="d-flex gap-2 ms-auto pb-1">
      <button class="btn tb" type="button" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Last saved 2 minutes ago">Save draft</button>
      <button class="btn tb primary" type="button" id="tt-publish" aria-disabled="true" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Add a cover image before publishing">Publish</button>
    </div>
  </div>
</div></div>
CSS
/* Bootstrap's Tooltip plugin, restyled through its CSS variables. The script below triggers it manually
   so it also stays open while the pointer is on it and closes with Esc. */
.tooltip.tt{--bs-tooltip-bg:#1b2032;--bs-tooltip-color:#f8fafc;--bs-tooltip-opacity:1;--bs-tooltip-max-width:240px;--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:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
.tooltip.tt .tooltip-inner{font-weight:500;line-height:1.45;box-shadow:0 10px 24px -10px rgba(15,23,42,.55)}
.tooltip.tt-start .tooltip-inner{text-align:left}

/* Demo triggers */
.tb{--bs-btn-padding-x:16px;--bs-btn-padding-y:0;--bs-btn-font-size:.875rem;--bs-btn-font-weight:600;--bs-btn-line-height:1;--bs-btn-color:#1f2433;--bs-btn-bg:#fff;
  --bs-btn-border-color:#cdd3de;--bs-btn-border-radius:10px;--bs-btn-hover-color:#1f2433;--bs-btn-hover-bg:#f5f6f9;--bs-btn-hover-border-color:#b9c1cf;
  --bs-btn-active-color:#1f2433;--bs-btn-active-bg:#eef0f4;--bs-btn-active-border-color:#b9c1cf;display:inline-flex;align-items:center;height:40px}
.tb.primary{--bs-btn-color:#fff;--bs-btn-bg:#4f46e5;--bs-btn-border-color:#4f46e5;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#4338ca;--bs-btn-hover-border-color:#4338ca;
  --bs-btn-active-color:#fff;--bs-btn-active-bg:#4338ca;--bs-btn-active-border-color:#4338ca}
.tb[aria-disabled="true"]{--bs-btn-bg:#eef0f4;--bs-btn-border-color:#dfe3ea;--bs-btn-color:#5f6878;--bs-btn-hover-bg:#eef0f4;--bs-btn-hover-border-color:#dfe3ea;
  --bs-btn-hover-color:#5f6878;--bs-btn-active-bg:#eef0f4;--bs-btn-active-color:#5f6878;cursor:not-allowed}
.tb:focus-visible,.term:focus-visible{outline:2px solid #4f46e5;outline-offset:2px;box-shadow:none}
.dirs{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}
@media (max-width:600px){.dirs{grid-template-columns:1fr}}
.cell{display:flex;align-items:center;justify-content:center;height:140px;padding:0 20px;border:1px dashed #d6dbe4;border-radius:12px;background:#f8f9fb}
.cell.to-right{justify-content:flex-start}
.cell.to-left{justify-content:flex-end}
.term{padding:0;border:0;background:none;color:#3730a3;font:inherit;font-weight:600;text-decoration:underline dotted 1.5px;text-underline-offset:4px;cursor:help;border-radius:3px}
.prose{max-width:560px;margin:0;padding-top:58px;font-size:1rem;line-height:1.75;color:#343a4a}
.post{margin-bottom:48px;padding:16px;border:1px solid #e1e4eb;border-radius:14px;background:#fafbfc}
.post small i{width:8px;height:8px;border-radius:50%;background:#d97706}
/* 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}.tooltip.tt,.tb{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 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],
      customClass: ('tt ' + (el.dataset.bsCustomClass || '')).trim(),
      popperConfig: c => el.dataset.ttAlign === 'start' ? { placement: c.placement + '-start' } : {}
    });
    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
<div><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Four directions</span>
<div class="grid grid-cols-2 gap-3 rounded-[18px] border border-[#e1e4eb] bg-white p-7 max-[600px]:grid-cols-1 max-[600px]:p-5">
  <div class="flex h-[140px] items-center rounded-xl border border-dashed border-[#d6dbe4] bg-[#f8f9fb] px-5 justify-center"><span class="tt relative inline-flex align-middle"><button class="inline-flex h-10 cursor-pointer items-center gap-2 rounded-[10px] border border-[#cdd3de] bg-white px-4 text-sm leading-none font-semibold text-[#1f2433] transition-colors duration-150 hover:border-[#b9c1cf] hover:bg-[#f5f6f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" type="button" aria-describedby="tt-dup">Duplicate</button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] 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(15,23,42,.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] text-center 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]" role="tooltip" id="tt-dup">Copies the page and its comments</span></span></div>
  <div class="flex h-[140px] items-center rounded-xl border border-dashed border-[#d6dbe4] bg-[#f8f9fb] px-5 justify-start"><span class="tt relative inline-flex align-middle"><button class="inline-flex h-10 cursor-pointer items-center gap-2 rounded-[10px] border border-[#cdd3de] bg-white px-4 text-sm leading-none font-semibold text-[#1f2433] transition-colors duration-150 hover:border-[#b9c1cf] hover:bg-[#f5f6f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" type="button" aria-describedby="tt-move">Move</button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] 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(15,23,42,.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] text-center left-[calc(100%+10px)] top-1/2 -translate-y-1/2 before:right-full before:top-[calc(50%-6px)] before:border-l-0 before:border-r-tt after:inset-y-0 after:right-full after:w-[10px]" role="tooltip" id="tt-move">Pick another folder</span></span></div>
  <div class="flex h-[140px] items-center rounded-xl border border-dashed border-[#d6dbe4] bg-[#f8f9fb] px-5 justify-center"><span class="tt relative inline-flex align-middle"><button class="inline-flex h-10 cursor-pointer items-center gap-2 rounded-[10px] border border-[#cdd3de] bg-white px-4 text-sm leading-none font-semibold text-[#1f2433] transition-colors duration-150 hover:border-[#b9c1cf] hover:bg-[#f5f6f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" type="button" aria-describedby="tt-export">Export</button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] 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(15,23,42,.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] text-center tt-bottom top-[calc(100%+10px)] left-1/2 translate-x-[calc(-50%+var(--tt-nudge,0px))] before:bottom-full before:left-[calc(50%-6px-var(--tt-nudge,0px))] before:border-t-0 before:border-b-tt after:inset-x-0 after:bottom-full after:h-[10px]" role="tooltip" id="tt-export">PDF, PNG or SVG</span></span></div>
  <div class="flex h-[140px] items-center rounded-xl border border-dashed border-[#d6dbe4] bg-[#f8f9fb] px-5 justify-end"><span class="tt relative inline-flex align-middle"><button class="inline-flex h-10 cursor-pointer items-center gap-2 rounded-[10px] border border-[#cdd3de] bg-white px-4 text-sm leading-none font-semibold text-[#1f2433] transition-colors duration-150 hover:border-[#b9c1cf] hover:bg-[#f5f6f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" type="button" aria-describedby="tt-archive">Archive</button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] 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(15,23,42,.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] text-center right-[calc(100%+10px)] top-1/2 -translate-y-1/2 before:left-full before:top-[calc(50%-6px)] before:border-r-0 before:border-l-tt after:inset-y-0 after:left-full after:w-[10px]" role="tooltip" id="tt-archive">Restorable for 30 days</span></span></div>
</div></div>

<div><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Definitions inside text</span>
<div class="rounded-[18px] border border-[#e1e4eb] bg-white p-7 max-[600px]:p-5">
  <p class="prose m-0 max-w-[560px] pt-[58px] text-base leading-[1.75] text-[#343a4a]">A <span class="tt relative inline-flex align-middle"><button class="cursor-help rounded-[3px] font-semibold text-[#3730a3] underline decoration-dotted decoration-[1.5px] underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" aria-describedby="tt-reserve">rolling reserve</button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] 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(15,23,42,.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] text-left tt-top bottom-[calc(100%+10px)] left-0 translate-x-[var(--tt-nudge,0px)] before:top-full before:left-[calc(14px-var(--tt-nudge,0px))] before:border-b-0 before:border-t-tt after:inset-x-0 after:top-full after:h-[10px]" role="tooltip" id="tt-reserve">A share of each payout held back and released automatically after 90 days.</span></span> of 5% is held from every payout to cover <span class="tt relative inline-flex align-middle"><button class="cursor-help rounded-[3px] font-semibold text-[#3730a3] underline decoration-dotted decoration-[1.5px] underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" aria-describedby="tt-chargeback">chargebacks</button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] 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(15,23,42,.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] text-left tt-top bottom-[calc(100%+10px)] left-0 translate-x-[var(--tt-nudge,0px)] before:top-full before:left-[calc(14px-var(--tt-nudge,0px))] before:border-b-0 before:border-t-tt after:inset-x-0 after:top-full after:h-[10px]" role="tooltip" id="tt-chargeback">A refund your customer's bank forces after a dispute, plus a $15 fee.</span></span>. Everything else reaches your bank two business days after the sale.</p>
</div></div>

<div><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Explaining a disabled action</span>
<div class="rounded-[18px] border border-[#e1e4eb] bg-white p-7 max-[600px]:p-5">
  <div class="mb-12 flex flex-wrap items-center gap-3 rounded-[14px] border border-[#e1e4eb] bg-[#fafbfc] p-4">
    <div><b class="block text-[.95rem]">Five rituals of slow mornings</b><small class="flex items-center gap-1.5 text-[.8125rem] text-[#5b6474]"><i class="size-2 rounded-full bg-[#d97706]" aria-hidden="true"></i>Draft · cover image missing</small></div>
    <div class="acts ml-auto flex gap-2.5 pb-1">
      <span class="tt relative inline-flex align-middle"><button class="inline-flex h-10 cursor-pointer items-center gap-2 rounded-[10px] border border-[#cdd3de] bg-white px-4 text-sm leading-none font-semibold text-[#1f2433] transition-colors duration-150 hover:border-[#b9c1cf] hover:bg-[#f5f6f9] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] motion-reduce:transition-none" type="button" aria-describedby="tt-save">Save draft</button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] 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(15,23,42,.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] text-center tt-bottom top-[calc(100%+10px)] left-1/2 translate-x-[calc(-50%+var(--tt-nudge,0px))] before:bottom-full before:left-[calc(50%-6px-var(--tt-nudge,0px))] before:border-t-0 before:border-b-tt after:inset-x-0 after:bottom-full after:h-[10px]" role="tooltip" id="tt-save">Last saved 2 minutes ago</span></span>
      <span class="tt relative inline-flex align-middle"><button class="inline-flex h-10 cursor-not-allowed items-center rounded-[10px] border border-[#dfe3ea] bg-[#eef0f4] px-4 text-sm leading-none font-semibold text-[#5f6878] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" type="button" aria-disabled="true" aria-describedby="tt-publish">Publish</button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] 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(15,23,42,.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] text-center tt-bottom top-[calc(100%+10px)] left-1/2 translate-x-[calc(-50%+var(--tt-nudge,0px))] before:bottom-full before:left-[calc(50%-6px-var(--tt-nudge,0px))] before:border-t-0 before:border-b-tt after:inset-x-0 after:bottom-full after:h-[10px]" role="tooltip" id="tt-publish">Add a cover image before publishing</span></span>
    </div>
  </div>
</div></div>
CSS
@theme {
  --color-tt: #1b2032;
  --color-tt-ink: #f8fafc;
}
/* 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 top/bottom tooltips are nudged sideways to stay inside the window.
(() => {
  const tts = [...document.querySelectorAll('.tt')];
  const fit = t => {
    const tip = t.querySelector(':scope > .tt-top, :scope > .tt-bottom'); 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

The tooltips most interfaces need, each with an arrow that points at its trigger. Four directions shows Duplicate, Move, Export and Archive buttons with tooltips above, to the right, below and to the left. Definitions inside text puts dotted-underline terms such as rolling reserve and chargebacks in a paragraph about payouts, each a real button whose tooltip explains the word and lines up with its start. Explaining a disabled action pairs Save draft with a Publish button that uses aria-disabled instead of disabled, so it can still be focused and hovered, and its tooltip says why it cannot be used; the reason also appears as visible text on the card. Each tooltip sits in a wrapper next to its trigger, and CSS shows it on hover and on keyboard focus. A short script adds what CSS cannot do: Esc hides the tooltip without moving focus, a tap opens it on touch screens, only one is open at a time, and top and bottom tooltips are nudged sideways to stay inside the window while the arrow keeps pointing at the trigger. The Bootstrap version uses Bootstrap’s own Tooltip plugin.

What’s included

  • Top, right, bottom and left placements with CSS arrows
  • Definition tooltips on words inside a paragraph
  • A focusable aria-disabled button that explains why it is disabled
  • Shows on hover and keyboard focus, hides with Esc
  • Stays open while the pointer moves onto the tooltip
  • Bootstrap version built on the Bootstrap Tooltip plugin

Accessibility

Each trigger is a native button linked to its tooltip with aria-describedby, and the tooltip has role="tooltip". Tooltips open on keyboard focus as well as hover, Esc dismisses them without moving focus, and an invisible bridge keeps them open while the pointer moves onto them, as WCAG 1.4.13 asks. The disabled reason is also visible text, and every button shows a 2px focus outline. The fade is removed under prefers-reduced-motion.

Customise it

Change --tt-bg, --tt-ink, --tt-gap, --tt-arrow and --tt-max on .tt (plain CSS) or the --color-tt and --color-tt-ink theme colours (Tailwind). In Bootstrap the same values are --bs-tooltip-bg, --bs-tooltip-color, --bs-tooltip-max-width and the arrow variables on .tooltip.tt.