Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-bold" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z"/></symbol>
  <symbol id="i-italic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10 5h8M6 19h8M14.5 5l-5 14"/></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>
  <symbol id="i-list" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M9 6h11M9 12h11M9 18h11"/><circle cx="4.5" cy="6" r="1" fill="currentColor"/><circle cx="4.5" cy="12" r="1" fill="currentColor"/><circle cx="4.5" cy="18" r="1" fill="currentColor"/></symbol>
  <symbol id="i-quote" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 7h14M9 12h10M9 17h10M5 11v7"/></symbol>
  <symbol id="i-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><circle cx="9" cy="10" r="1.8"/><path d="m20.5 16-5-5-9 8.5"/></symbol>
</svg>

<div><span class="lbl">Icon buttons named by their tooltip</span>
<div class="stage" style="--stage-h:240px">
  <div class="bar" role="group" aria-label="Text formatting" style="margin-top:48px">
    <span class="tt"><button class="ib" type="button" aria-pressed="true" aria-labelledby="tip-bold"><svg aria-hidden="true"><use href="#i-bold"/></svg></button><span class="tt-tip tt-bottom" role="tooltip" id="tip-bold">Bold</span></span>
    <span class="tt"><button class="ib" type="button" aria-pressed="false" aria-labelledby="tip-italic"><svg aria-hidden="true"><use href="#i-italic"/></svg></button><span class="tt-tip tt-bottom" role="tooltip" id="tip-italic">Italic</span></span>
    <span class="sep" aria-hidden="true"></span>
    <span class="tt" id="tt-link"><button class="ib" type="button" aria-labelledby="tip-link"><svg aria-hidden="true"><use href="#i-link"/></svg></button><span class="tt-tip tt-bottom" role="tooltip" id="tip-link">Insert link</span></span>
    <span class="tt"><button class="ib" type="button" aria-labelledby="tip-list"><svg aria-hidden="true"><use href="#i-list"/></svg></button><span class="tt-tip tt-bottom" role="tooltip" id="tip-list">Bulleted list</span></span>
    <span class="tt"><button class="ib" type="button" aria-labelledby="tip-quote"><svg aria-hidden="true"><use href="#i-quote"/></svg></button><span class="tt-tip tt-bottom" role="tooltip" id="tip-quote">Quote</span></span>
    <span class="tt"><button class="ib" type="button" aria-labelledby="tip-image"><svg aria-hidden="true"><use href="#i-image"/></svg></button><span class="tt-tip tt-bottom" role="tooltip" id="tip-image">Add image</span></span>
  </div>
</div></div>

<div><span class="lbl">Help toggletip on a form field</span>
<div class="stage" style="--stage-h:240px">
  <div class="field" style="margin-top:64px">
    <span class="field-top"><label for="f-vat">VAT number</label>
      <span class="tg" id="tg-vat-wrap"><button class="help" type="button" aria-label="About the VAT number" aria-expanded="false" aria-controls="tg-vat" data-toggletip>?</button><span class="tt-tip tt-top" id="tg-vat">We print it on every invoice so your accountant can reclaim VAT. Leave it empty if you buy as a private person.</span><span class="sr-only" role="status"></span></span></span>
    <input id="f-vat" aria-describedby="f-vat-hint" placeholder="DE123456789" autocomplete="off">
    <small id="f-vat-hint">Starts with your two-letter country code.</small>
  </div>
  <div class="field"><label for="f-company">Company name</label><input id="f-company" value="Halden Ceramics GmbH" autocomplete="organization"></div>
</div></div>

<div class="full"><span class="lbl">Full text of a truncated name</span>
<div class="stage">
  <ul class="files">
    <li><span class="ft" aria-hidden="true">PDF</span><span class="tt tr" id="tt-file"><a class="trunc" href="#files/board-pack">Northwind-board-pack-Q3-2026-final-signed-with-appendices.pdf</a><span class="tt-tip tt-top" aria-hidden="true">Northwind-board-pack-Q3-2026-final-signed-with-appendices.pdf</span></span><span class="meta">4.2 MB</span></li>
    <li><span class="ft doc" aria-hidden="true">DOC</span><span class="tt tr"><a class="trunc" href="#files/interviews">Customer-interviews-Lisbon-and-Porto-transcripts-September.docx</a><span class="tt-tip tt-top" aria-hidden="true">Customer-interviews-Lisbon-and-Porto-transcripts-September.docx</span></span><span class="meta">880 KB</span></li>
    <li><span class="ft fig" aria-hidden="true">FIG</span><span class="tt tr"><a class="trunc" href="#files/moodboard">brand-refresh-moodboard-v7-approved-by-marketing-and-legal.fig</a><span class="tt-tip tt-top" aria-hidden="true">brand-refresh-moodboard-v7-approved-by-marketing-and-legal.fig</span></span><span class="meta">12 MB</span></li>
  </ul>
</div></div>
CSS
/* Tooltip core: CSS shows .tt-tip on hover and keyboard focus; the script adds Esc, tap to open and the window-edge nudge. */
.tt,.tg{--tt-bg:#111b30;--tt-ink:#f5f8ff;--tt-gap:10px;--tt-arrow:6px;position:relative;display:inline-flex;vertical-align:middle}
.tt-tip{position:absolute;z-index:30;width:max-content;max-width:240px;padding:6px 10px;border-radius:7px;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,.tg.is-open>.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-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)}

/* Icon toolbar: each button is named by its tooltip (aria-labelledby) */
.bar{display:inline-flex;flex-wrap:wrap;gap:2px;padding:4px;border:1px solid #d5dce8;border-radius:12px;background:#fff;box-shadow:0 1px 2px rgba(15,23,42,.05)}
.bar .sep{width:1px;margin:6px 4px;background:#dce2ec}
.ib{display:grid;place-items:center;width:36px;height:36px;padding:0;border:0;border-radius:8px;background:none;color:#3b4760;cursor:pointer;transition:background-color .15s,color .15s}
.ib:hover{background:#eef2f8;color:#18233a}
.ib[aria-pressed="true"]{background:#e3ecff;color:#1d4ed8}
.ib svg{width:18px;height:18px}

/* Help toggletip: a "?" button that opens a bubble on click and announces it */
.field{display:grid;gap:6px;margin:0 0 18px}
.field:last-child{margin-bottom:0}
.field-top{display:flex;align-items:center;gap:6px}
.field label{font-size:.875rem;font-weight:600}
.field input{height:42px;padding:0 12px;border:1px solid #8d99ae;border-radius:9px;background:#fff;color:#18233a;font:inherit;font-size:.9375rem}
.field input:focus-visible{outline:2px solid #2563eb;outline-offset:1px;border-color:#2563eb}
.field small{color:#536079;font-size:.8125rem}
.help{display:grid;place-items:center;width:20px;height:20px;padding:0;border:1.5px solid #6b7890;border-radius:50%;background:#fff;color:#3b4760;
  font:700 .75rem/1 ui-sans-serif,system-ui,sans-serif;cursor:pointer}
.help:hover,.help[aria-expanded="true"]{border-color:#2563eb;background:#2563eb;color:#fff}
.tg .tt-tip{text-align:left;font-weight:400;max-width:250px;padding:8px 11px}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}

/* Truncated text: the tooltip only appears when the name is cut off; on touch screens the name wraps instead */
.files{margin:0;padding:0;list-style:none;max-width:440px}
.files li{display:flex;align-items:center;gap:12px;padding:10px 0;border-top:1px solid #e6ebf2}
.files li:first-child{border-top:0}
.files .ft{display:grid;place-items:center;flex:none;width:34px;height:40px;border-radius:7px;background:#e3ecff;color:#1d4ed8;font:800 .625rem/1 ui-sans-serif,system-ui,sans-serif;letter-spacing:.04em}
.files .ft.doc{background:#dff3ff;color:#075985}.files .ft.fig{background:#f3e8ff;color:#7e22ce}
.files .meta{flex:none;color:#536079;font-size:.8125rem}
.tt.tr{display:flex;flex:1;min-width:0}
.trunc{display:block;min-width:0;overflow:hidden;color:#18233a;font-weight:600;font-size:.9rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;border-radius:4px}
.trunc:hover{color:#1d4ed8;text-decoration:underline}
.tt.tr .tt-tip{max-width:300px;overflow-wrap:anywhere}
.tt.tr:not([data-clipped])>.tt-tip{display:none}
@media (hover:none){.trunc{white-space:normal;overflow-wrap:anywhere}}

:is(.ib,.help,.trunc):focus-visible{outline:2px solid #2563eb;outline-offset:2px}
@media (prefers-reduced-motion:reduce){.tt .tt-tip,.tg .tt-tip,.tt:is(:hover,:focus-within,.is-open):not(.is-dismissed)>.tt-tip,.tg.is-open>.tt-tip,.ib{transition:none}}
JavaScript
(() => {
  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');
    });
  };
  // Truncated names: mark the ones that are actually cut off, so only those get a tooltip.
  const clip = () => document.querySelectorAll('.tt.tr').forEach(t => { const a = t.firstElementChild; t.toggleAttribute('data-clipped', a.scrollWidth > a.clientWidth); });
  clip(); addEventListener('resize', clip);

  // Tooltips (WCAG 1.4.13): Esc dismisses without moving focus, a tap opens one on touch screens, one open at a time.
  const tts = [...document.querySelectorAll('.tt')];
  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); });

  // Toggletips: the "?" button opens its bubble on click and a status region reads the text out; Esc or a click elsewhere closes it.
  const tgs = [...document.querySelectorAll('.tg')];
  const setTip = (tg, open) => {
    const btn = tg.querySelector('[data-toggletip]'), live = tg.querySelector('[role="status"]');
    tg.classList.toggle('is-open', open); btn.setAttribute('aria-expanded', open);
    live.textContent = '';
    if (open) { fit(tg); setTimeout(() => { live.textContent = document.getElementById(btn.getAttribute('aria-controls')).textContent; }, 60); }
  };
  tgs.forEach(tg => tg.querySelector('[data-toggletip]').addEventListener('click', () => setTip(tg, !tg.classList.contains('is-open'))));

  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');
    tgs.forEach(tg => !tg.contains(e.target) && tg.classList.contains('is-open') && setTip(tg, false));
  });
  document.addEventListener('keydown', e => {
    if (e.key !== 'Escape') return;
    tgs.forEach(tg => tg.classList.contains('is-open') && setTip(tg, false));
    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
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-bold" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z"/></symbol>
  <symbol id="i-italic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10 5h8M6 19h8M14.5 5l-5 14"/></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>
  <symbol id="i-list" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M9 6h11M9 12h11M9 18h11"/><circle cx="4.5" cy="6" r="1" fill="currentColor"/><circle cx="4.5" cy="12" r="1" fill="currentColor"/><circle cx="4.5" cy="18" r="1" fill="currentColor"/></symbol>
  <symbol id="i-quote" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 7h14M9 12h10M9 17h10M5 11v7"/></symbol>
  <symbol id="i-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><circle cx="9" cy="10" r="1.8"/><path d="m20.5 16-5-5-9 8.5"/></symbol>
</svg>

<div><span class="lbl">Icon buttons named by their tooltip</span>
<div class="stage" style="--stage-h:240px">
  <div class="bar" role="group" aria-label="Text formatting" style="margin-top:48px">
    <button class="btn ib" type="button" aria-pressed="true" aria-label="Bold" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Bold" data-tt-label><svg aria-hidden="true"><use href="#i-bold"/></svg></button>
    <button class="btn ib" type="button" aria-pressed="false" aria-label="Italic" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Italic" data-tt-label><svg aria-hidden="true"><use href="#i-italic"/></svg></button>
    <span class="vr" aria-hidden="true"></span>
    <button class="btn ib" type="button" id="tt-link" aria-label="Insert link" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Insert link" data-tt-label><svg aria-hidden="true"><use href="#i-link"/></svg></button>
    <button class="btn ib" type="button" aria-label="Bulleted list" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Bulleted list" data-tt-label><svg aria-hidden="true"><use href="#i-list"/></svg></button>
    <button class="btn ib" type="button" aria-label="Quote" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Quote" data-tt-label><svg aria-hidden="true"><use href="#i-quote"/></svg></button>
    <button class="btn ib" type="button" aria-label="Add image" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Add image" data-tt-label><svg aria-hidden="true"><use href="#i-image"/></svg></button>
  </div>
</div></div>

<div><span class="lbl">Help toggletip on a form field</span>
<div class="stage" style="--stage-h:240px">
  <div style="margin-top:64px">
    <div class="d-flex align-items-center gap-2 mb-2"><label class="form-label" for="f-vat">VAT number</label>
      <button class="help" type="button" id="tg-vat-btn" aria-label="About the VAT number" aria-expanded="false" data-bs-toggle="popover" data-bs-placement="top"
        data-bs-content="We print it on every invoice so your accountant can reclaim VAT. Leave it empty if you buy as a private person.">?</button><span class="visually-hidden" role="status"></span></div>
    <input class="form-control" id="f-vat" aria-describedby="f-vat-hint" placeholder="DE123456789" autocomplete="off">
    <div class="form-text" id="f-vat-hint">Starts with your two-letter country code.</div>
  </div>
  <div class="d-grid gap-2 mt-3"><label class="form-label" for="f-company">Company name</label><input class="form-control" id="f-company" value="Halden Ceramics GmbH" autocomplete="organization"></div>
</div></div>

<div class="full"><span class="lbl">Full text of a truncated name</span>
<div class="stage">
  <ul class="list-group list-group-flush files">
    <li class="list-group-item d-flex align-items-center gap-3"><span class="ft" aria-hidden="true">PDF</span><a class="trunc text-truncate" id="tt-file" href="#files/board-pack" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-custom-class="tt-file" data-bs-title="Northwind-board-pack-Q3-2026-final-signed-with-appendices.pdf" data-tt-label data-tt-clip>Northwind-board-pack-Q3-2026-final-signed-with-appendices.pdf</a><span class="meta">4.2 MB</span></li>
    <li class="list-group-item d-flex align-items-center gap-3"><span class="ft doc" aria-hidden="true">DOC</span><a class="trunc text-truncate" href="#files/interviews" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-custom-class="tt-file" data-bs-title="Customer-interviews-Lisbon-and-Porto-transcripts-September.docx" data-tt-label data-tt-clip>Customer-interviews-Lisbon-and-Porto-transcripts-September.docx</a><span class="meta">880 KB</span></li>
    <li class="list-group-item d-flex align-items-center gap-3"><span class="ft fig" aria-hidden="true">FIG</span><a class="trunc text-truncate" href="#files/moodboard" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-custom-class="tt-file" data-bs-title="brand-refresh-moodboard-v7-approved-by-marketing-and-legal.fig" data-tt-label data-tt-clip>brand-refresh-moodboard-v7-approved-by-marketing-and-legal.fig</a><span class="meta">12 MB</span></li>
  </ul>
</div></div>
CSS
/* Bootstrap Tooltip for icon buttons and truncated names, Bootstrap Popover for the "?" toggletip, restyled through their CSS variables.
   The script triggers them so they open on hover, focus and tap, stay open under the pointer and close with Esc. */
.tooltip.tt{--bs-tooltip-bg:#111b30;--bs-tooltip-color:#f5f8ff;--bs-tooltip-opacity:1;--bs-tooltip-max-width:240px;--bs-tooltip-padding-x:10px;--bs-tooltip-padding-y:6px;
  --bs-tooltip-font-size:.8125rem;--bs-tooltip-border-radius:7px;--bs-tooltip-arrow-width:12px;--bs-tooltip-arrow-height:6px;font-family:inherit}
.tooltip.tt .tooltip-inner{font-weight:500;line-height:1.45;box-shadow:0 10px 24px -10px rgba(15,23,42,.55)}
.tooltip.tt-file{--bs-tooltip-max-width:300px}
.tooltip.tt-file .tooltip-inner{overflow-wrap:anywhere}
.popover.tg-pop{--bs-popover-bg:#111b30;--bs-popover-border-color:#111b30;--bs-popover-arrow-border:#111b30;--bs-popover-max-width:250px;--bs-popover-font-size:.8125rem;
  --bs-popover-border-radius:7px;--bs-popover-inner-border-radius:7px;--bs-popover-body-padding-x:11px;--bs-popover-body-padding-y:8px;--bs-popover-body-color:#f5f8ff;
  --bs-popover-arrow-width:12px;--bs-popover-arrow-height:6px;font-family:inherit;line-height:1.45;box-shadow:0 10px 24px -10px rgba(15,23,42,.55)}

/* Icon toolbar */
.bar{display:inline-flex;flex-wrap:wrap;gap:2px;padding:4px;border:1px solid #d5dce8;border-radius:12px;background:#fff;box-shadow:0 1px 2px rgba(15,23,42,.05)}
.bar .vr{margin:6px 4px;background:#dce2ec;opacity:1}
.ib{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-color:#3b4760;--bs-btn-border-width:0;--bs-btn-border-radius:8px;--bs-btn-hover-bg:#eef2f8;--bs-btn-hover-color:#18233a;
  --bs-btn-active-bg:#e3ecff;--bs-btn-active-color:#1d4ed8;display:grid;place-items:center;width:36px;height:36px}
.ib[aria-pressed="true"]{background:#e3ecff;color:#1d4ed8}
.ib svg{width:18px;height:18px}

/* Form */
.form-label{margin:0;font-size:.875rem;font-weight:600}
.form-control{--bs-border-color:#8d99ae;height:42px;border-radius:9px;font-size:.9375rem;color:#18233a}
.form-control:focus{border-color:#2563eb;box-shadow:none}
.form-control:focus-visible{outline:2px solid #2563eb;outline-offset:1px}
.form-text{margin-top:6px;color:#536079;font-size:.8125rem}
.help{display:grid;place-items:center;width:20px;height:20px;padding:0;border:1.5px solid #6b7890;border-radius:50%;background:#fff;color:#3b4760;font-size:.75rem;font-weight:700;line-height:1}
.help:hover,.help[aria-expanded="true"]{border-color:#2563eb;background:#2563eb;color:#fff}

/* Files */
.files{max-width:440px;--bs-list-group-border-color:#e6ebf2;--bs-list-group-item-padding-x:0;--bs-list-group-item-padding-y:10px}
.ft{display:grid;place-items:center;flex:none;width:34px;height:40px;border-radius:7px;background:#e3ecff;color:#1d4ed8;font-size:.625rem;font-weight:800;line-height:1;letter-spacing:.04em}
.ft.doc{background:#dff3ff;color:#075985}.ft.fig{background:#f3e8ff;color:#7e22ce}
.trunc{display:block;flex:1;min-width:0;color:#18233a;font-weight:600;font-size:.9rem;text-decoration:none;border-radius:4px}
.trunc:hover{color:#1d4ed8;text-decoration:underline}
.meta{flex:none;color:#536079;font-size:.8125rem}
@media (hover:none){.trunc.text-truncate{white-space:normal;overflow-wrap:anywhere}}
:is(.ib,.help,.trunc):focus-visible{outline:2px solid #2563eb;outline-offset:2px;box-shadow:none}
/* 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,.popover,.ib{transition:none}}
JavaScript
(() => {
  // Bootstrap Tooltip, triggered here (WCAG 1.4.13): hover, focus and tap open it, it stays open under the pointer and Esc closes it.
  // data-tt-label: the tooltip repeats the button's aria-label, so Bootstrap's aria-describedby is dropped to avoid reading it twice.
  // data-tt-clip: only show the tooltip when the text is actually cut off.
  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: ('tt ' + (el.dataset.bsCustomClass || '')).trim() });
    tips.set(el, tip);
    let open = false, timer;
    el.addEventListener('show.bs.tooltip', e => { if ('ttClip' in el.dataset && el.scrollWidth <= el.clientWidth) e.preventDefault(); else open = true; });
    el.addEventListener('hide.bs.tooltip', () => { open = false; });
    el.addEventListener('inserted.bs.tooltip', () => {
      if ('ttLabel' in el.dataset) el.removeAttribute('aria-describedby');
      tip.tip.addEventListener('mouseenter', () => clearTimeout(timer));
      tip.tip.addEventListener('mouseleave', leave);
    });
    const show = () => { clearTimeout(timer); hideTips(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(); });
  });

  // Toggletips: Bootstrap Popover opened by click; a visually hidden status region reads the text out. Esc or a click elsewhere closes it.
  const pops = [];
  document.querySelectorAll('[data-bs-toggle="popover"]').forEach(btn => {
    const pop = new bootstrap.Popover(btn, { trigger: 'click', offset: [0, 10], customClass: 'tg-pop', container: btn.parentElement });
    const live = btn.parentElement.querySelector('[role="status"]');
    pops.push([btn, pop]);
    btn.addEventListener('shown.bs.popover', () => {
      btn.setAttribute('aria-expanded', 'true'); btn.removeAttribute('aria-describedby');
      setTimeout(() => { live.textContent = btn.dataset.bsContent; }, 60);
    });
    btn.addEventListener('hide.bs.popover', () => { btn.setAttribute('aria-expanded', 'false'); live.textContent = ''; });
  });
  const closePops = () => pops.forEach(([btn, pop]) => btn.getAttribute('aria-expanded') === 'true' && pop.hide());

  document.addEventListener('pointerdown', e => {
    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') { hideTips(); closePops(); } });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-bold" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z"/></symbol>
  <symbol id="i-italic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10 5h8M6 19h8M14.5 5l-5 14"/></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>
  <symbol id="i-list" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M9 6h11M9 12h11M9 18h11"/><circle cx="4.5" cy="6" r="1" fill="currentColor"/><circle cx="4.5" cy="12" r="1" fill="currentColor"/><circle cx="4.5" cy="18" r="1" fill="currentColor"/></symbol>
  <symbol id="i-quote" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 7h14M9 12h10M9 17h10M5 11v7"/></symbol>
  <symbol id="i-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><circle cx="9" cy="10" r="1.8"/><path d="m20.5 16-5-5-9 8.5"/></symbol>
</svg>

<div><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#536079] uppercase">Icon buttons named by their tooltip</span>
<div class="rounded-[18px] border border-[#dce2ec] bg-white p-7 max-[600px]:p-5 min-h-[240px]">
  <div class="mt-12 inline-flex flex-wrap gap-0.5 rounded-xl border border-[#d5dce8] bg-white p-1 shadow-[0_1px_2px_rgba(15,23,42,.05)]" role="group" aria-label="Text formatting">
    <span class="tt relative inline-flex align-middle"><button class="grid size-9 cursor-pointer place-items-center rounded-lg border-0 bg-transparent p-0 text-[#3b4760] transition-colors duration-150 hover:bg-[#eef2f8] hover:text-[#18233a] aria-pressed:bg-[#e3ecff] aria-pressed:text-[#1d4ed8] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" type="button" aria-pressed="true" aria-labelledby="tip-bold"><svg class="size-[18px]" aria-hidden="true"><use href="#i-bold"/></svg></button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] rounded-[7px] bg-tt px-2.5 py-1.5 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="tip-bold">Bold</span></span>
    <span class="tt relative inline-flex align-middle"><button class="grid size-9 cursor-pointer place-items-center rounded-lg border-0 bg-transparent p-0 text-[#3b4760] transition-colors duration-150 hover:bg-[#eef2f8] hover:text-[#18233a] aria-pressed:bg-[#e3ecff] aria-pressed:text-[#1d4ed8] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" type="button" aria-pressed="false" aria-labelledby="tip-italic"><svg class="size-[18px]" aria-hidden="true"><use href="#i-italic"/></svg></button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] rounded-[7px] bg-tt px-2.5 py-1.5 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="tip-italic">Italic</span></span>
    <span class="mx-1 my-1.5 w-px bg-[#dce2ec]" aria-hidden="true"></span>
    <span class="tt relative inline-flex align-middle" id="tt-link"><button class="grid size-9 cursor-pointer place-items-center rounded-lg border-0 bg-transparent p-0 text-[#3b4760] transition-colors duration-150 hover:bg-[#eef2f8] hover:text-[#18233a] aria-pressed:bg-[#e3ecff] aria-pressed:text-[#1d4ed8] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" type="button" aria-labelledby="tip-link"><svg class="size-[18px]" aria-hidden="true"><use href="#i-link"/></svg></button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] rounded-[7px] bg-tt px-2.5 py-1.5 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="tip-link">Insert link</span></span>
    <span class="tt relative inline-flex align-middle"><button class="grid size-9 cursor-pointer place-items-center rounded-lg border-0 bg-transparent p-0 text-[#3b4760] transition-colors duration-150 hover:bg-[#eef2f8] hover:text-[#18233a] aria-pressed:bg-[#e3ecff] aria-pressed:text-[#1d4ed8] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" type="button" aria-labelledby="tip-list"><svg class="size-[18px]" aria-hidden="true"><use href="#i-list"/></svg></button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] rounded-[7px] bg-tt px-2.5 py-1.5 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="tip-list">Bulleted list</span></span>
    <span class="tt relative inline-flex align-middle"><button class="grid size-9 cursor-pointer place-items-center rounded-lg border-0 bg-transparent p-0 text-[#3b4760] transition-colors duration-150 hover:bg-[#eef2f8] hover:text-[#18233a] aria-pressed:bg-[#e3ecff] aria-pressed:text-[#1d4ed8] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" type="button" aria-labelledby="tip-quote"><svg class="size-[18px]" aria-hidden="true"><use href="#i-quote"/></svg></button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] rounded-[7px] bg-tt px-2.5 py-1.5 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="tip-quote">Quote</span></span>
    <span class="tt relative inline-flex align-middle"><button class="grid size-9 cursor-pointer place-items-center rounded-lg border-0 bg-transparent p-0 text-[#3b4760] transition-colors duration-150 hover:bg-[#eef2f8] hover:text-[#18233a] aria-pressed:bg-[#e3ecff] aria-pressed:text-[#1d4ed8] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" type="button" aria-labelledby="tip-image"><svg class="size-[18px]" aria-hidden="true"><use href="#i-image"/></svg></button><span class="pointer-events-auto absolute z-30 w-max max-w-[240px] rounded-[7px] bg-tt px-2.5 py-1.5 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="tip-image">Add image</span></span>
  </div>
</div></div>

<div><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#536079] uppercase">Help toggletip on a form field</span>
<div class="rounded-[18px] border border-[#dce2ec] bg-white p-7 max-[600px]:p-5 min-h-[240px]">
  <div class="mt-16 mb-[18px] grid gap-1.5">
    <span class="flex items-center gap-1.5"><label class="text-sm font-semibold" for="f-vat">VAT number</label>
      <span class="tg relative inline-flex align-middle" id="tg-vat-wrap"><button class="grid size-5 cursor-pointer place-items-center rounded-full border-[1.5px] border-[#6b7890] bg-white p-0 text-xs leading-none font-bold text-[#3b4760] hover:border-[#2563eb] hover:bg-[#2563eb] hover:text-white aria-expanded:border-[#2563eb] aria-expanded:bg-[#2563eb] aria-expanded:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" type="button" aria-label="About the VAT number" aria-expanded="false" aria-controls="tg-vat" data-toggletip>?</button><span class="pointer-events-auto absolute z-30 w-max max-w-[250px] rounded-[7px] bg-tt px-[11px] py-2 text-[.8125rem] leading-[1.45] font-normal 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 tg-open:visible tg-open:opacity-100 tg-open:scale-100 motion-safe:[transition:opacity_.15s,visibility_.15s,scale_0s_.15s] motion-safe:tg-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" id="tg-vat">We print it on every invoice so your accountant can reclaim VAT. Leave it empty if you buy as a private person.</span><span class="sr-only" role="status"></span></span></span>
    <input class="h-[42px] rounded-[9px] border border-[#8d99ae] bg-white px-3 text-[.9375rem] text-[#18233a] focus-visible:border-[#2563eb] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#2563eb]" id="f-vat" aria-describedby="f-vat-hint" placeholder="DE123456789" autocomplete="off">
    <small class="text-[.8125rem] text-[#536079]" id="f-vat-hint">Starts with your two-letter country code.</small>
  </div>
  <div class="grid gap-1.5"><label class="text-sm font-semibold" for="f-company">Company name</label><input class="h-[42px] rounded-[9px] border border-[#8d99ae] bg-white px-3 text-[.9375rem] text-[#18233a] focus-visible:border-[#2563eb] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#2563eb]" id="f-company" value="Halden Ceramics GmbH" autocomplete="organization"></div>
</div></div>

<div class="col-span-full"><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#536079] uppercase">Full text of a truncated name</span>
<div class="rounded-[18px] border border-[#dce2ec] bg-white p-7 max-[600px]:p-5">
  <ul class="m-0 max-w-[440px] list-none p-0">
    <li class="flex items-center gap-3 border-t border-[#e6ebf2] py-2.5 first:border-t-0"><span class="grid h-10 w-[34px] flex-none place-items-center rounded-[7px] text-[.625rem] leading-none font-extrabold tracking-[.04em] bg-[#e3ecff] text-[#1d4ed8]" aria-hidden="true">PDF</span><span class="tt tr relative flex min-w-0 flex-1" id="tt-file"><a class="block min-w-0 truncate rounded text-[.9rem] font-semibold text-[#18233a] no-underline hover:text-[#1d4ed8] hover:underline [@media(hover:none)]:whitespace-normal [@media(hover:none)]:wrap-anywhere focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" href="#files/board-pack">Northwind-board-pack-Q3-2026-final-signed-with-appendices.pdf</a><span class="pointer-events-auto absolute z-30 w-max max-w-[300px] rounded-[7px] bg-tt px-2.5 py-1.5 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] wrap-anywhere [.tt:not([data-clipped])>&]:hidden" aria-hidden="true">Northwind-board-pack-Q3-2026-final-signed-with-appendices.pdf</span></span><span class="flex-none text-[.8125rem] text-[#536079]">4.2 MB</span></li>
    <li class="flex items-center gap-3 border-t border-[#e6ebf2] py-2.5 first:border-t-0"><span class="grid h-10 w-[34px] flex-none place-items-center rounded-[7px] text-[.625rem] leading-none font-extrabold tracking-[.04em] bg-[#dff3ff] text-[#075985]" aria-hidden="true">DOC</span><span class="tt tr relative flex min-w-0 flex-1"><a class="block min-w-0 truncate rounded text-[.9rem] font-semibold text-[#18233a] no-underline hover:text-[#1d4ed8] hover:underline [@media(hover:none)]:whitespace-normal [@media(hover:none)]:wrap-anywhere focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" href="#files/interviews">Customer-interviews-Lisbon-and-Porto-transcripts-September.docx</a><span class="pointer-events-auto absolute z-30 w-max max-w-[300px] rounded-[7px] bg-tt px-2.5 py-1.5 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] wrap-anywhere [.tt:not([data-clipped])>&]:hidden" aria-hidden="true">Customer-interviews-Lisbon-and-Porto-transcripts-September.docx</span></span><span class="flex-none text-[.8125rem] text-[#536079]">880 KB</span></li>
    <li class="flex items-center gap-3 border-t border-[#e6ebf2] py-2.5 first:border-t-0"><span class="grid h-10 w-[34px] flex-none place-items-center rounded-[7px] text-[.625rem] leading-none font-extrabold tracking-[.04em] bg-[#f3e8ff] text-[#7e22ce]" aria-hidden="true">FIG</span><span class="tt tr relative flex min-w-0 flex-1"><a class="block min-w-0 truncate rounded text-[.9rem] font-semibold text-[#18233a] no-underline hover:text-[#1d4ed8] hover:underline [@media(hover:none)]:whitespace-normal [@media(hover:none)]:wrap-anywhere focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" href="#files/moodboard">brand-refresh-moodboard-v7-approved-by-marketing-and-legal.fig</a><span class="pointer-events-auto absolute z-30 w-max max-w-[300px] rounded-[7px] bg-tt px-2.5 py-1.5 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] wrap-anywhere [.tt:not([data-clipped])>&]:hidden" aria-hidden="true">brand-refresh-moodboard-v7-approved-by-marketing-and-legal.fig</span></span><span class="flex-none text-[.8125rem] text-[#536079]">12 MB</span></li>
  </ul>
</div></div>
CSS
@theme {
  --color-tt: #111b30;
  --color-tt-ink: #f5f8ff;
}
/* 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) > *));
/* tg-open: the bubble of a toggletip whose "?" button was clicked. */
@custom-variant tg-open (&:is(:where(.tg).is-open > *));
JavaScript
(() => {
  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');
    });
  };
  // Truncated names: mark the ones that are actually cut off, so only those get a tooltip.
  const clip = () => document.querySelectorAll('.tt.tr').forEach(t => { const a = t.firstElementChild; t.toggleAttribute('data-clipped', a.scrollWidth > a.clientWidth); });
  clip(); addEventListener('resize', clip);

  // Tooltips (WCAG 1.4.13): Esc dismisses without moving focus, a tap opens one on touch screens, one open at a time.
  const tts = [...document.querySelectorAll('.tt')];
  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); });

  // Toggletips: the "?" button opens its bubble on click and a status region reads the text out; Esc or a click elsewhere closes it.
  const tgs = [...document.querySelectorAll('.tg')];
  const setTip = (tg, open) => {
    const btn = tg.querySelector('[data-toggletip]'), live = tg.querySelector('[role="status"]');
    tg.classList.toggle('is-open', open); btn.setAttribute('aria-expanded', open);
    live.textContent = '';
    if (open) { fit(tg); setTimeout(() => { live.textContent = document.getElementById(btn.getAttribute('aria-controls')).textContent; }, 60); }
  };
  tgs.forEach(tg => tg.querySelector('[data-toggletip]').addEventListener('click', () => setTip(tg, !tg.classList.contains('is-open'))));

  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');
    tgs.forEach(tg => !tg.contains(e.target) && tg.classList.contains('is-open') && setTip(tg, false));
  });
  document.addEventListener('keydown', e => {
    if (e.key !== 'Escape') return;
    tgs.forEach(tg => tg.classList.contains('is-open') && setTip(tg, false));
    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

Three places where a tooltip does real work. Icon buttons named by their tooltip is a text formatting toolbar, with Bold, Italic, Insert link, Bulleted list, Quote and Add image, where each icon-only button takes its accessible name from its tooltip through aria-labelledby, so the name is written once and read the same way by screen readers. Help toggletip on a form field puts a question mark button next to VAT number: a click opens a bubble explaining why the number is asked for, and a status region reads it out. The rule people must follow, that it starts with the country code, stays visible as hint text under the field instead of hiding in the bubble. Full text of a truncated name lists shared files whose long names are cut off with an ellipsis; the tooltip shows the full name, but only when the name is actually cut, and on touch screens the names wrap instead. The Bootstrap version uses Bootstrap Tooltip for the toolbar and the file names and Bootstrap Popover for the toggletip.

What’s included

  • Icon buttons labelled by their tooltip with aria-labelledby
  • A click toggletip whose text is announced through role="status"
  • Essential help kept as visible hint text
  • Tooltip only on file names that are really truncated
  • Names wrap instead on touch screens
  • Esc closes tooltips and the toggletip

Accessibility

Toolbar buttons are named by their tooltips, so screen readers say Bold, toggle button, pressed, and the tooltip is not read twice; Bootstrap drops its extra aria-describedby for the same reason. The toggletip button has an aria-label and aria-expanded, and its text is announced in a status region when it opens. File links already contain the full name, so their tooltips are hidden from screen readers. Esc dismisses everything, focus outlines are visible and the fades stop under prefers-reduced-motion.

Customise it

Set --tt-bg and --tt-ink on .tt and .tg in plain CSS, the --bs-tooltip-* and --bs-popover-* variables on .tooltip.tt and .popover.tg-pop in Bootstrap, and the --color-tt theme colours in Tailwind. The help button colour is the #2563eb hover and expanded state.