Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-undo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></symbol>
  <symbol id="i-redo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="m15 14 5-5-5-5"/><path d="M20 9H9.5a5.5 5.5 0 0 0 0 11H13"/></symbol>
  <symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><circle cx="11" cy="11" r="6.5"/><path d="m20 20-4.2-4.2"/></symbol>
  <symbol id="i-comment" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 18.5V6.5A2.5 2.5 0 0 1 7.5 4h9A2.5 2.5 0 0 1 19 6.5v7a2.5 2.5 0 0 1-2.5 2.5H9z"/></symbol>
  <symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></symbol>
</svg>

<div><span class="lbl">Tooltips with keyboard shortcuts</span>
<div class="stage" style="padding-bottom:72px">
  <div class="tools" role="group" aria-label="Editing">
    <span class="tt" id="tt-undo"><button class="ib" type="button" aria-labelledby="k-undo" aria-describedby="k-undo-keys" aria-keyshortcuts="Meta+Z"><svg aria-hidden="true"><use href="#i-undo"/></svg></button>
      <span class="tt-tip tt-bottom" role="tooltip"><span id="k-undo">Undo</span><span class="keys" id="k-undo-keys"><kbd><span aria-hidden="true">⌘</span><span class="sr-only">Command</span></kbd> <kbd>Z</kbd></span></span></span>
    <span class="tt"><button class="ib" type="button" aria-labelledby="k-redo" aria-describedby="k-redo-keys" aria-keyshortcuts="Shift+Meta+Z"><svg aria-hidden="true"><use href="#i-redo"/></svg></button>
      <span class="tt-tip tt-bottom" role="tooltip"><span id="k-redo">Redo</span><span class="keys" id="k-redo-keys"><kbd><span aria-hidden="true">⇧</span><span class="sr-only">Shift</span></kbd> <kbd><span aria-hidden="true">⌘</span><span class="sr-only">Command</span></kbd> <kbd>Z</kbd></span></span></span>
    <span class="tt"><button class="ib" type="button" aria-labelledby="k-find" aria-describedby="k-find-keys" aria-keyshortcuts="Meta+F"><svg aria-hidden="true"><use href="#i-search"/></svg></button>
      <span class="tt-tip tt-bottom" role="tooltip"><span id="k-find">Find in document</span><span class="keys" id="k-find-keys"><kbd><span aria-hidden="true">⌘</span><span class="sr-only">Command</span></kbd> <kbd>F</kbd></span></span></span>
    <span class="tt" id="tt-comment"><button class="ib" type="button" aria-labelledby="k-comment" aria-describedby="k-comment-keys" aria-keyshortcuts="Alt+Meta+M"><svg aria-hidden="true"><use href="#i-comment"/></svg></button>
      <span class="tt-tip tt-bottom" role="tooltip"><span id="k-comment">Add comment</span><span class="keys" id="k-comment-keys"><kbd><span aria-hidden="true">⌥</span><span class="sr-only">Option</span></kbd> <kbd><span aria-hidden="true">⌘</span><span class="sr-only">Command</span></kbd> <kbd>M</kbd></span></span></span>
  </div>
</div></div>

<div><span class="lbl">Onboarding coach marks</span>
<div class="stage">
  <div class="app tour">
    <div class="app-bar">
      <span class="logo"><i aria-hidden="true"></i><span>Ledgerly</span></span>
      <div class="cm-at"><button class="btn" type="button" aria-label="Search"><svg aria-hidden="true"><use href="#i-search"/></svg></button>
        <div class="cm" id="cm-1" role="dialog" aria-labelledby="cm-1-t" aria-describedby="cm-1-d" tabindex="-1" hidden>
          <small>Step 1 of 3</small><h3 id="cm-1-t">Find anything</h3><p id="cm-1-d">Search clients, invoices and payments. Press <kbd>⌘</kbd> <kbd>K</kbd> from any page.</p>
          <div class="cm-foot"><span class="cm-dots" aria-hidden="true"><i class="on"></i><i></i><i></i></span><button class="ghost" type="button" data-step="skip">Skip tour</button><button class="go" type="button" data-step="next">Next</button></div></div></div>
      <div class="cm-at"><button class="btn solid" type="button"><svg aria-hidden="true"><use href="#i-plus"/></svg>New invoice</button>
        <div class="cm" id="cm-2" role="dialog" aria-labelledby="cm-2-t" aria-describedby="cm-2-d" tabindex="-1" hidden>
          <small>Step 2 of 3</small><h3 id="cm-2-t">Send your first invoice</h3><p id="cm-2-d">Add a client and line items. Ledgerly works out VAT and the due date for you.</p>
          <div class="cm-foot"><span class="cm-dots" aria-hidden="true"><i></i><i class="on"></i><i></i></span><button class="ghost" type="button" data-step="back">Back</button><button class="go" type="button" data-step="next">Next</button></div></div></div>
      <div class="cm-at"><button class="avatar" type="button" aria-label="Account: Jonas Berg">JB</button>
        <div class="cm" id="cm-3" role="dialog" aria-labelledby="cm-3-t" aria-describedby="cm-3-d" tabindex="-1" hidden>
          <small>Step 3 of 3</small><h3 id="cm-3-t">Add your bank details</h3><p id="cm-3-d">Put your logo and IBAN in Settings so every invoice is ready to send.</p>
          <div class="cm-foot"><span class="cm-dots" aria-hidden="true"><i></i><i></i><i class="on"></i></span><button class="ghost" type="button" data-step="back">Back</button><button class="go" type="button" data-step="next">Done</button></div></div></div>
    </div>
    <div class="empty"><b>No invoices yet</b>Your sent and paid invoices will show up here.<br><button class="btn" type="button" aria-controls="cm-1" aria-expanded="false" data-tour-start>Take the tour</button></div>
  </div>
</div></div>
CSS
/* Shortcut tooltips: the core .tt tooltip with <kbd> keys. The button is named by the label part and described by the keys. */
.tt{--tt-bg:#1f1933;--tt-ink:#f7f5ff;--tt-gap:10px;--tt-arrow:6px;position:relative;display:inline-flex;vertical-align:middle}
.tt-tip{position:absolute;z-index:30;display:flex;align-items:center;gap:10px;width:max-content;max-width:260px;padding:6px 7px 6px 11px;border-radius:9px;background:var(--tt-bg);color:var(--tt-ink);
  font:500 .8125rem/1.4 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;white-space:nowrap;box-shadow:0 10px 24px -10px rgba(31,25,51,.6);
  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-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)}
.keys{display:inline-flex;gap:3px}
.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}
.keys kbd{display:inline-grid;place-items:center;min-width:22px;height:22px;padding:0 5px;border:1px solid #4a4266;border-bottom-width:2px;border-radius:5px;background:#2c2545;color:#ece9f8;
  font:600 .75rem/1 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}

.tools{display:inline-flex;gap:6px;padding:6px;border:1px solid #e2ddef;border-radius:12px;background:#faf9fd}
.ib{display:grid;place-items:center;width:38px;height:38px;padding:0;border:1px solid transparent;border-radius:9px;background:none;color:#463e60;cursor:pointer;transition:background-color .15s}
.ib:hover{background:#efebfa;color:#1f1933}
.ib svg{width:18px;height:18px}

/* Coach marks: one step of an onboarding tour at a time, pointing at the control it explains */
.cm-at{position:relative;display:inline-flex}
.cm-at::after{content:"";position:absolute;inset:-5px;border:2px solid #7c3aed;border-radius:12px;opacity:0;pointer-events:none}
.cm-at.is-current::after{opacity:1;animation:cm-pulse 1.8s ease-out infinite}
@keyframes cm-pulse{0%{box-shadow:0 0 0 0 rgba(124,58,237,.45)}70%,100%{box-shadow:0 0 0 10px rgba(124,58,237,0)}}
.cm{position:absolute;z-index:40;top:calc(100% + 16px);left:50%;translate:calc(-50% + var(--tt-nudge,0px)) 0;width:290px;max-width:calc(100vw - 32px);padding:16px 16px 14px;
  border-radius:14px;background:#2e1065;color:#f5f3ff;text-align:left;font-size:.875rem;line-height:1.5;box-shadow:0 24px 48px -16px rgba(46,16,101,.55);animation:cm-in .2s ease-out}
.cm[hidden]{display:none}
.cm::before{content:"";position:absolute;bottom:100%;left:calc(50% - 8px - var(--tt-nudge,0px));border:8px solid transparent;border-top-width:0;border-bottom-color:#2e1065}
.cm:focus-visible{outline:2px solid #a78bfa;outline-offset:3px}
@keyframes cm-in{from{opacity:0;translate:calc(-50% + var(--tt-nudge,0px)) 6px}}
.cm small{display:block;color:#c4b5fd;font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase}
.cm h3{margin:4px 0 4px;font-size:1rem;color:#fff}
.cm p{margin:0;color:#ddd6fe}
.cm kbd{padding:1px 5px;border:1px solid #6d4fc4;border-radius:4px;background:#3b1a80;color:#fff;font:600 .75rem ui-sans-serif,system-ui,sans-serif}
.cm-foot{display:flex;align-items:center;gap:8px;margin-top:14px}
.cm-dots{display:flex;gap:5px;margin-right:auto}
.cm-dots i{width:7px;height:7px;border-radius:50%;background:#6d4fc4}
.cm-dots i.on{width:18px;border-radius:4px;background:#ddd6fe}
.cm button{height:32px;padding:0 12px;border:0;border-radius:8px;font:600 .8125rem/1 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;cursor:pointer}
.cm .ghost{background:none;color:#ddd6fe}
.cm .ghost:hover{background:#3b1a80;color:#fff}
.cm .go{background:#fff;color:#4c1d95}
.cm .go:hover{background:#ede9fe}

/* Demo app bar */
.app{border:1px solid #e2ddef;border-radius:14px;background:#faf9fd}
.app-bar{display:flex;align-items:center;gap:10px;padding:12px 14px;border-bottom:1px solid #e2ddef;background:#fff;border-radius:14px 14px 0 0}
.logo{display:flex;align-items:center;gap:8px;margin-right:auto;font-weight:800;letter-spacing:-.01em}
.logo i{width:26px;height:26px;border-radius:8px;background:linear-gradient(135deg,#7c3aed,#c026d3)}
.btn{display:inline-flex;align-items:center;gap:8px;height:36px;padding:0 13px;border:1px solid #d9d2ec;border-radius:9px;background:#fff;color:#1f1933;
  font:600 .8125rem/1 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;cursor:pointer}
.btn:hover{background:#f4f1fb}
.btn.solid{background:#6d28d9;border-color:#6d28d9;color:#fff}
.btn.solid:hover{background:#5b21b6}
.btn svg{width:16px;height:16px}
.avatar{display:grid;place-items:center;width:36px;height:36px;padding:0;border:0;border-radius:50%;background:#fde68a;color:#713f12;font-weight:700;font-size:.8rem;cursor:pointer}
.empty{padding:196px 20px 28px;text-align:center;color:#5d5675;font-size:.875rem}
.empty b{display:block;color:#1f1933;font-size:.95rem}
.empty .btn{margin-top:12px}
:is(.ib,.btn,.avatar,.cm button):focus-visible{outline:2px solid #6d28d9;outline-offset:2px}
.cm button:focus-visible{outline-color:#c4b5fd}
@media (max-width:480px){.logo span{display:none}}
@media (prefers-reduced-motion:reduce){.tt .tt-tip,.tt:is(:hover,:focus-within,.is-open):not(.is-dismissed)>.tt-tip,.ib{transition:none}.cm,.cm-at.is-current::after{animation:none}}
JavaScript
(() => {
  // Keeps a tooltip or coach mark inside the window by nudging it sideways (the arrow stays on its target).
  const fit = tip => {
    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');
    });
  };

  // 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.querySelector('[role="tooltip"]')); };
  tts.forEach(t => { t.addEventListener('pointerenter', () => enter(t)); t.addEventListener('focusin', () => enter(t)); if (t.classList.contains('is-open')) fit(t.querySelector('[role="tooltip"]')); });
  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');
  });

  // Coach-mark tour: one step at a time. Next and Back move between steps and focus the new one; Skip, Done or Esc end the tour.
  const tour = document.querySelector('.tour');
  const steps = tour ? [...tour.querySelectorAll('.cm')] : [];
  const start = document.querySelector('[data-tour-start]');
  let cur = -1;
  const go = (i, focus = true) => {
    steps.forEach((s, n) => { s.hidden = n !== i; s.parentElement.classList.toggle('is-current', n === i); });
    cur = i;
    if (start) start.setAttribute('aria-expanded', i >= 0);
    if (i >= 0) { fit(steps[i]); if (focus) steps[i].focus(); }
  };
  const end = () => { const inside = cur >= 0 && steps[cur].contains(document.activeElement); go(-1); if (inside && start) start.focus(); };
  if (tour) tour.addEventListener('click', e => {
    const b = e.target.closest('[data-step]'); if (!b) return;
    if (b.dataset.step === 'next') cur < steps.length - 1 ? go(cur + 1) : end();
    else if (b.dataset.step === 'back') go(cur - 1);
    else end();
  });
  if (start) start.addEventListener('click', () => go(0));

  document.addEventListener('keydown', e => {
    if (e.key !== 'Escape') return;
    if (cur >= 0) { end(); 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
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-undo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></symbol>
  <symbol id="i-redo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="m15 14 5-5-5-5"/><path d="M20 9H9.5a5.5 5.5 0 0 0 0 11H13"/></symbol>
  <symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><circle cx="11" cy="11" r="6.5"/><path d="m20 20-4.2-4.2"/></symbol>
  <symbol id="i-comment" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 18.5V6.5A2.5 2.5 0 0 1 7.5 4h9A2.5 2.5 0 0 1 19 6.5v7a2.5 2.5 0 0 1-2.5 2.5H9z"/></symbol>
  <symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></symbol>
</svg>

<div><span class="lbl">Tooltips with keyboard shortcuts</span>
<div class="stage" style="padding-bottom:72px">
  <div class="tools" role="group" aria-label="Editing">
    <button class="btn ib" type="button" aria-label="Undo" aria-keyshortcuts="Meta+Z" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-html="true"
      data-bs-title='<span aria-hidden="true">Undo</span><span class="keys"><kbd><span aria-hidden="true">⌘</span><span class="visually-hidden">Command</span></kbd> <kbd>Z</kbd></span>'><svg aria-hidden="true"><use href="#i-undo"/></svg></button>
    <button class="btn ib" type="button" aria-label="Redo" aria-keyshortcuts="Shift+Meta+Z" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-html="true"
      data-bs-title='<span aria-hidden="true">Redo</span><span class="keys"><kbd><span aria-hidden="true">⇧</span><span class="visually-hidden">Shift</span></kbd> <kbd><span aria-hidden="true">⌘</span><span class="visually-hidden">Command</span></kbd> <kbd>Z</kbd></span>'><svg aria-hidden="true"><use href="#i-redo"/></svg></button>
    <button class="btn ib" type="button" aria-label="Find in document" aria-keyshortcuts="Meta+F" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-html="true"
      data-bs-title='<span aria-hidden="true">Find in document</span><span class="keys"><kbd><span aria-hidden="true">⌘</span><span class="visually-hidden">Command</span></kbd> <kbd>F</kbd></span>'><svg aria-hidden="true"><use href="#i-search"/></svg></button>
    <button class="btn ib" type="button" id="tt-comment" aria-label="Add comment" aria-keyshortcuts="Alt+Meta+M" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-html="true"
      data-bs-title='<span aria-hidden="true">Add comment</span><span class="keys"><kbd><span aria-hidden="true">⌥</span><span class="visually-hidden">Option</span></kbd> <kbd><span aria-hidden="true">⌘</span><span class="visually-hidden">Command</span></kbd> <kbd>M</kbd></span>'><svg aria-hidden="true"><use href="#i-comment"/></svg></button>
  </div>
</div></div>

<div><span class="lbl">Onboarding coach marks</span>
<div class="stage">
  <div class="tour border rounded-4" style="--bs-border-color:#e2ddef;background:#faf9fd">
    <div class="d-flex align-items-center gap-2 px-3 py-2 bg-white border-bottom rounded-top-4" style="--bs-border-color:#e2ddef;padding-top:12px!important;padding-bottom:12px!important">
      <span class="d-flex align-items-center gap-2 me-auto fw-bolder"><i class="logo-mark" aria-hidden="true"></i><span class="logo-name">Ledgerly</span></span>
      <span class="cm-at"><button class="btn btn-app" type="button" aria-label="Search" data-tour-step="cm-1" data-bs-title="<small>Step 1 of 3</small>Find anything"><svg aria-hidden="true"><use href="#i-search"/></svg></button></span>
      <span class="cm-at"><button class="btn btn-app solid" type="button" data-tour-step="cm-2" data-bs-title="<small>Step 2 of 3</small>Send your first invoice"><svg aria-hidden="true"><use href="#i-plus"/></svg>New invoice</button></span>
      <span class="cm-at"><button class="avatar rounded-circle" type="button" aria-label="Account: Jonas Berg" data-tour-step="cm-3" data-bs-title="<small>Step 3 of 3</small>Add your bank details">JB</button></span>
    </div>
    <div class="text-center px-3" style="padding-top:196px;padding-bottom:28px;color:#5d5675;font-size:.875rem"><b class="d-block" style="color:#1f1933;font-size:.95rem">No invoices yet</b>Your sent and paid invoices will show up here.<br>
      <button class="btn btn-app mt-3" type="button" aria-expanded="false" data-tour-start>Take the tour</button></div>
  </div>
  <template id="cm-1"><p>Search clients, invoices and payments. Press <kbd>⌘</kbd> <kbd>K</kbd> from any page.</p>
    <div class="d-flex align-items-center gap-2 mt-3"><span class="cm-dots" aria-hidden="true"><i class="on"></i><i></i><i></i></span><button class="btn cm-btn" type="button" data-step="skip">Skip tour</button><button class="btn cm-btn go" type="button" data-step="next">Next</button></div></template>
  <template id="cm-2"><p>Add a client and line items. Ledgerly works out VAT and the due date for you.</p>
    <div class="d-flex align-items-center gap-2 mt-3"><span class="cm-dots" aria-hidden="true"><i></i><i class="on"></i><i></i></span><button class="btn cm-btn" type="button" data-step="back">Back</button><button class="btn cm-btn go" type="button" data-step="next">Next</button></div></template>
  <template id="cm-3"><p>Put your logo and IBAN in Settings so every invoice is ready to send.</p>
    <div class="d-flex align-items-center gap-2 mt-3"><span class="cm-dots" aria-hidden="true"><i></i><i></i><i class="on"></i></span><button class="btn cm-btn" type="button" data-step="back">Back</button><button class="btn cm-btn go" type="button" data-step="next">Done</button></div></template>
</div></div>
CSS
/* Shortcut tooltips: Bootstrap Tooltip with <kbd> keys in its HTML title. Coach marks: Bootstrap Popover, one step at a time.
   Both are restyled through Bootstrap's CSS variables and triggered by the script below. */
.tooltip.tt{--bs-tooltip-bg:#1f1933;--bs-tooltip-color:#f7f5ff;--bs-tooltip-opacity:1;--bs-tooltip-max-width:260px;--bs-tooltip-padding-x:7px;--bs-tooltip-padding-y:6px;
  --bs-tooltip-font-size:.8125rem;--bs-tooltip-border-radius:9px;--bs-tooltip-arrow-width:12px;--bs-tooltip-arrow-height:6px;font-family:inherit}
.tooltip.tt .tooltip-inner{display:flex;align-items:center;gap:10px;padding-left:11px;font-weight:500;line-height:1.4;white-space:nowrap;box-shadow:0 10px 24px -10px rgba(31,25,51,.6)}
.keys{display:inline-flex;gap:3px}
.keys kbd{display:inline-grid;place-items:center;min-width:22px;height:22px;padding:0 5px;border:1px solid #4a4266;border-bottom-width:2px;border-radius:5px;background:#2c2545;color:#ece9f8;
  font-family:inherit;font-size:.75rem;font-weight:600;line-height:1;box-shadow:none}
.tools{display:inline-flex;gap:6px;padding:6px;border:1px solid #e2ddef;border-radius:12px;background:#faf9fd}
.ib{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-color:#463e60;--bs-btn-border-color:transparent;--bs-btn-border-radius:9px;--bs-btn-hover-bg:#efebfa;--bs-btn-hover-color:#1f1933;
  --bs-btn-hover-border-color:transparent;--bs-btn-active-bg:#e6e0f7;--bs-btn-active-border-color:transparent;display:grid;place-items:center;width:38px;height:38px}
.ib svg{width:18px;height:18px}

.cm-at{position:relative;display:inline-flex}
.cm-at::after{content:"";position:absolute;inset:-5px;border:2px solid #7c3aed;border-radius:12px;opacity:0;pointer-events:none}
.cm-at.is-current::after{opacity:1;animation:cm-pulse 1.8s ease-out infinite}
@keyframes cm-pulse{0%{box-shadow:0 0 0 0 rgba(124,58,237,.45)}70%,100%{box-shadow:0 0 0 10px rgba(124,58,237,0)}}
.popover.cm{--bs-popover-bg:#2e1065;--bs-popover-border-color:#2e1065;--bs-popover-arrow-border:#2e1065;--bs-popover-max-width:290px;--bs-popover-font-size:.875rem;
  --bs-popover-border-radius:14px;--bs-popover-inner-border-radius:14px;--bs-popover-header-bg:transparent;--bs-popover-header-color:#fff;--bs-popover-header-font-size:1rem;
  --bs-popover-header-padding-x:16px;--bs-popover-header-padding-y:16px;--bs-popover-body-padding-x:16px;--bs-popover-body-padding-y:14px;--bs-popover-body-color:#ddd6fe;
  --bs-popover-arrow-width:16px;--bs-popover-arrow-height:8px;width:min(290px,calc(100vw - 32px));font-family:inherit;line-height:1.5;
  box-shadow:0 24px 48px -16px rgba(46,16,101,.55);animation:cm-in .2s ease-out}
@keyframes cm-in{from{opacity:0}}
.popover.cm:focus-visible{outline:2px solid #a78bfa;outline-offset:3px}
.popover.cm .popover-header{padding-bottom:0;border:0;font-weight:700}
.popover.cm .popover-header small{display:block;margin-bottom:4px;color:#c4b5fd;font-size:.75rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase}
.popover.cm .popover-body{padding-top:4px}
.popover.cm p{margin:0}
.popover.cm kbd{padding:1px 5px;border:1px solid #6d4fc4;border-radius:4px;background:#3b1a80;color:#fff;font-size:.75rem;font-weight:600}
.cm-dots{display:flex;gap:5px;margin-right:auto}
.cm-dots i{width:7px;height:7px;border-radius:50%;background:#6d4fc4}
.cm-dots i.on{width:18px;border-radius:4px;background:#ddd6fe}
.cm-btn{--bs-btn-padding-x:12px;--bs-btn-font-size:.8125rem;--bs-btn-font-weight:600;--bs-btn-line-height:1;--bs-btn-border-width:0;--bs-btn-border-radius:8px;height:32px;
  --bs-btn-color:#ddd6fe;--bs-btn-hover-bg:#3b1a80;--bs-btn-hover-color:#fff;--bs-btn-active-bg:#3b1a80;--bs-btn-active-color:#fff}
.cm-btn.go{--bs-btn-color:#4c1d95;--bs-btn-bg:#fff;--bs-btn-hover-bg:#ede9fe;--bs-btn-hover-color:#4c1d95;--bs-btn-active-bg:#ede9fe;--bs-btn-active-color:#4c1d95}

.btn-app{--bs-btn-padding-x:13px;--bs-btn-font-size:.8125rem;--bs-btn-font-weight:600;--bs-btn-line-height:1;--bs-btn-border-radius:9px;--bs-btn-color:#1f1933;--bs-btn-bg:#fff;
  --bs-btn-border-color:#d9d2ec;--bs-btn-hover-color:#1f1933;--bs-btn-hover-bg:#f4f1fb;--bs-btn-hover-border-color:#d9d2ec;--bs-btn-active-bg:#ece7f8;--bs-btn-active-border-color:#d9d2ec;
  display:inline-flex;align-items:center;gap:8px;height:36px}
.btn-app.solid{--bs-btn-color:#fff;--bs-btn-bg:#6d28d9;--bs-btn-border-color:#6d28d9;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5b21b6;--bs-btn-hover-border-color:#5b21b6;
  --bs-btn-active-color:#fff;--bs-btn-active-bg:#5b21b6;--bs-btn-active-border-color:#5b21b6}
.btn-app svg{width:16px;height:16px}
.avatar{display:grid;place-items:center;width:36px;height:36px;padding:0;border:0;background:#fde68a;color:#713f12;font-weight:700;font-size:.8rem}
.logo-mark{width:26px;height:26px;border-radius:8px;background:linear-gradient(135deg,#7c3aed,#c026d3)}
:is(.ib,.btn-app,.avatar,.cm-btn):focus-visible{outline:2px solid #6d28d9;outline-offset:2px;box-shadow:none}
.cm-btn:focus-visible{outline-color:#c4b5fd}
@media (max-width:480px){.logo-name{display:none}}
.tooltip.show{animation:tt-in .15s ease-out}
@keyframes tt-in{from{opacity:0}}
@media (prefers-reduced-motion:reduce){.tooltip.show,.popover.cm,.cm-at.is-current::after{animation:none}.btn{transition:none}}
JavaScript
(() => {
  // Shortcut tooltips: Bootstrap Tooltip (animation off, CSS fades it in), triggered here so it opens on hover, focus and tap,
  // stays open under the pointer and closes with Esc. kbd is added to Bootstrap's HTML allow-list.
  const allowList = { ...bootstrap.Tooltip.Default.allowList, kbd: [] };
  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', allowList });
    tips.set(el, tip);
    let open = false, timer;
    el.addEventListener('show.bs.tooltip', () => { open = true; });
    el.addEventListener('hide.bs.tooltip', () => { open = false; });
    el.addEventListener('inserted.bs.tooltip', () => {
      tip.tip.addEventListener('mouseenter', () => clearTimeout(timer));
      tip.tip.addEventListener('mouseleave', leave);
    });
    const show = () => { clearTimeout(timer); hideTips(tip); 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')) hideTips(); });

  // Coach-mark tour: a Bootstrap Popover per step, shown one at a time. Next and Back focus the new step; Skip, Done or Esc end the tour.
  const targets = [...document.querySelectorAll('[data-tour-step]')];
  const start = document.querySelector('[data-tour-start]');
  const steps = targets.map((t, i) => new bootstrap.Popover(t, {
    trigger: 'manual', html: true, animation: false, placement: 'bottom', offset: [0, 16], customClass: 'cm', allowList,
    template: `<div class="popover" role="dialog" aria-labelledby="cm-title-${i}"><div class="popover-arrow"></div><h3 class="popover-header" id="cm-title-${i}"></h3><div class="popover-body"></div></div>`,
    content: () => document.getElementById(t.dataset.tourStep).content.cloneNode(true)
  }));
  targets.forEach(t => t.addEventListener('inserted.bs.popover', () => t.removeAttribute('aria-describedby')));
  let cur = -1;
  const go = (i, focus = true) => {
    steps.forEach((p, n) => { if (n !== i) p.hide(); targets[n].parentElement.classList.toggle('is-current', n === i); });
    cur = i;
    start.setAttribute('aria-expanded', i >= 0);
    // Bootstrap's sanitizer strips tabindex from the template, so it is set here to let the step take focus.
    if (i >= 0) { steps[i].show(); steps[i].tip.tabIndex = -1; if (focus) steps[i].tip.focus(); }
  };
  const end = () => { const inside = cur >= 0 && steps[cur].tip && steps[cur].tip.contains(document.activeElement); go(-1); if (inside) start.focus(); };
  document.addEventListener('click', e => {
    const b = e.target.closest('.popover.cm [data-step]'); if (!b) return;
    if (b.dataset.step === 'next') cur < steps.length - 1 ? go(cur + 1) : end();
    else if (b.dataset.step === 'back') go(cur - 1);
    else end();
  });
  start.addEventListener('click', () => go(0));

  document.addEventListener('keydown', e => {
    if (e.key !== 'Escape') return;
    if (cur >= 0) end(); else hideTips();
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-undo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></symbol>
  <symbol id="i-redo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="m15 14 5-5-5-5"/><path d="M20 9H9.5a5.5 5.5 0 0 0 0 11H13"/></symbol>
  <symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><circle cx="11" cy="11" r="6.5"/><path d="m20 20-4.2-4.2"/></symbol>
  <symbol id="i-comment" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 18.5V6.5A2.5 2.5 0 0 1 7.5 4h9A2.5 2.5 0 0 1 19 6.5v7a2.5 2.5 0 0 1-2.5 2.5H9z"/></symbol>
  <symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></symbol>
</svg>

<div><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#5d5675] uppercase">Tooltips with keyboard shortcuts</span>
<div class="rounded-[18px] border border-[#e2ddef] bg-white p-7 max-[600px]:p-5 pb-[72px] max-[600px]:pb-[72px]">
  <div class="inline-flex gap-1.5 rounded-xl border border-[#e2ddef] bg-[#faf9fd] p-1.5" role="group" aria-label="Editing">
    <span class="tt relative inline-flex align-middle"><button class="grid size-[38px] cursor-pointer place-items-center rounded-[9px] border border-transparent bg-transparent p-0 text-[#463e60] transition-colors duration-150 hover:bg-[#efebfa] hover:text-[#1f1933] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#6d28d9]" type="button" aria-labelledby="k-undo" aria-describedby="k-undo-keys" aria-keyshortcuts="Meta+Z"><svg class="size-[18px]" aria-hidden="true"><use href="#i-undo"/></svg></button>
      <span class="pointer-events-auto absolute z-30 w-max max-w-[260px] rounded-[9px] bg-tt py-1.5 pr-[7px] pl-[11px] text-[.8125rem] leading-[1.45] font-medium text-tt-ink whitespace-nowrap shadow-[0_10px_24px_-10px_rgba(31,25,51,.6)] before:absolute before:border-transparent after:absolute before:border-[6px] invisible opacity-0 scale-0 tt-open:visible tt-open:opacity-100 tt-open:scale-100 motion-safe:[transition:opacity_.15s,visibility_.15s,scale_0s_.15s] motion-safe:tt-open:[transition:opacity_.15s,visibility_0s,scale_0s] tt-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] flex items-center gap-2.5" role="tooltip"><span id="k-undo">Undo</span><span class="inline-flex gap-[3px]" id="k-undo-keys"><kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-[5px] border border-b-2 border-[#4a4266] bg-[#2c2545] px-[5px] font-sans text-xs leading-none font-semibold text-[#ece9f8]"><span aria-hidden="true">⌘</span><span class="sr-only">Command</span></kbd> <kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-[5px] border border-b-2 border-[#4a4266] bg-[#2c2545] px-[5px] font-sans text-xs leading-none font-semibold text-[#ece9f8]">Z</kbd></span></span></span>
    <span class="tt relative inline-flex align-middle"><button class="grid size-[38px] cursor-pointer place-items-center rounded-[9px] border border-transparent bg-transparent p-0 text-[#463e60] transition-colors duration-150 hover:bg-[#efebfa] hover:text-[#1f1933] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#6d28d9]" type="button" aria-labelledby="k-redo" aria-describedby="k-redo-keys" aria-keyshortcuts="Shift+Meta+Z"><svg class="size-[18px]" aria-hidden="true"><use href="#i-redo"/></svg></button>
      <span class="pointer-events-auto absolute z-30 w-max max-w-[260px] rounded-[9px] bg-tt py-1.5 pr-[7px] pl-[11px] text-[.8125rem] leading-[1.45] font-medium text-tt-ink whitespace-nowrap shadow-[0_10px_24px_-10px_rgba(31,25,51,.6)] before:absolute before:border-transparent after:absolute before:border-[6px] invisible opacity-0 scale-0 tt-open:visible tt-open:opacity-100 tt-open:scale-100 motion-safe:[transition:opacity_.15s,visibility_.15s,scale_0s_.15s] motion-safe:tt-open:[transition:opacity_.15s,visibility_0s,scale_0s] tt-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] flex items-center gap-2.5" role="tooltip"><span id="k-redo">Redo</span><span class="inline-flex gap-[3px]" id="k-redo-keys"><kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-[5px] border border-b-2 border-[#4a4266] bg-[#2c2545] px-[5px] font-sans text-xs leading-none font-semibold text-[#ece9f8]"><span aria-hidden="true">⇧</span><span class="sr-only">Shift</span></kbd> <kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-[5px] border border-b-2 border-[#4a4266] bg-[#2c2545] px-[5px] font-sans text-xs leading-none font-semibold text-[#ece9f8]"><span aria-hidden="true">⌘</span><span class="sr-only">Command</span></kbd> <kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-[5px] border border-b-2 border-[#4a4266] bg-[#2c2545] px-[5px] font-sans text-xs leading-none font-semibold text-[#ece9f8]">Z</kbd></span></span></span>
    <span class="tt relative inline-flex align-middle"><button class="grid size-[38px] cursor-pointer place-items-center rounded-[9px] border border-transparent bg-transparent p-0 text-[#463e60] transition-colors duration-150 hover:bg-[#efebfa] hover:text-[#1f1933] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#6d28d9]" type="button" aria-labelledby="k-find" aria-describedby="k-find-keys" aria-keyshortcuts="Meta+F"><svg class="size-[18px]" aria-hidden="true"><use href="#i-search"/></svg></button>
      <span class="pointer-events-auto absolute z-30 w-max max-w-[260px] rounded-[9px] bg-tt py-1.5 pr-[7px] pl-[11px] text-[.8125rem] leading-[1.45] font-medium text-tt-ink whitespace-nowrap shadow-[0_10px_24px_-10px_rgba(31,25,51,.6)] before:absolute before:border-transparent after:absolute before:border-[6px] invisible opacity-0 scale-0 tt-open:visible tt-open:opacity-100 tt-open:scale-100 motion-safe:[transition:opacity_.15s,visibility_.15s,scale_0s_.15s] motion-safe:tt-open:[transition:opacity_.15s,visibility_0s,scale_0s] tt-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] flex items-center gap-2.5" role="tooltip"><span id="k-find">Find in document</span><span class="inline-flex gap-[3px]" id="k-find-keys"><kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-[5px] border border-b-2 border-[#4a4266] bg-[#2c2545] px-[5px] font-sans text-xs leading-none font-semibold text-[#ece9f8]"><span aria-hidden="true">⌘</span><span class="sr-only">Command</span></kbd> <kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-[5px] border border-b-2 border-[#4a4266] bg-[#2c2545] px-[5px] font-sans text-xs leading-none font-semibold text-[#ece9f8]">F</kbd></span></span></span>
    <span class="tt relative inline-flex align-middle" id="tt-comment"><button class="grid size-[38px] cursor-pointer place-items-center rounded-[9px] border border-transparent bg-transparent p-0 text-[#463e60] transition-colors duration-150 hover:bg-[#efebfa] hover:text-[#1f1933] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#6d28d9]" type="button" aria-labelledby="k-comment" aria-describedby="k-comment-keys" aria-keyshortcuts="Alt+Meta+M"><svg class="size-[18px]" aria-hidden="true"><use href="#i-comment"/></svg></button>
      <span class="pointer-events-auto absolute z-30 w-max max-w-[260px] rounded-[9px] bg-tt py-1.5 pr-[7px] pl-[11px] text-[.8125rem] leading-[1.45] font-medium text-tt-ink whitespace-nowrap shadow-[0_10px_24px_-10px_rgba(31,25,51,.6)] before:absolute before:border-transparent after:absolute before:border-[6px] invisible opacity-0 scale-0 tt-open:visible tt-open:opacity-100 tt-open:scale-100 motion-safe:[transition:opacity_.15s,visibility_.15s,scale_0s_.15s] motion-safe:tt-open:[transition:opacity_.15s,visibility_0s,scale_0s] tt-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] flex items-center gap-2.5" role="tooltip"><span id="k-comment">Add comment</span><span class="inline-flex gap-[3px]" id="k-comment-keys"><kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-[5px] border border-b-2 border-[#4a4266] bg-[#2c2545] px-[5px] font-sans text-xs leading-none font-semibold text-[#ece9f8]"><span aria-hidden="true">⌥</span><span class="sr-only">Option</span></kbd> <kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-[5px] border border-b-2 border-[#4a4266] bg-[#2c2545] px-[5px] font-sans text-xs leading-none font-semibold text-[#ece9f8]"><span aria-hidden="true">⌘</span><span class="sr-only">Command</span></kbd> <kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-[5px] border border-b-2 border-[#4a4266] bg-[#2c2545] px-[5px] font-sans text-xs leading-none font-semibold text-[#ece9f8]">M</kbd></span></span></span>
  </div>
</div></div>

<div><span class="mb-[14px] block text-[.72rem] tracking-[.12em] text-[#5d5675] uppercase">Onboarding coach marks</span>
<div class="rounded-[18px] border border-[#e2ddef] bg-white p-7 max-[600px]:p-5">
  <div class="tour rounded-[14px] border border-[#e2ddef] bg-[#faf9fd]">
    <div class="flex items-center gap-2.5 rounded-t-[14px] border-b border-[#e2ddef] bg-white px-3.5 py-3">
      <span class="mr-auto flex items-center gap-2 font-extrabold tracking-[-.01em]"><i class="size-[26px] rounded-lg bg-linear-135 from-[#7c3aed] to-[#c026d3]" aria-hidden="true"></i><span class="max-[480px]:hidden">Ledgerly</span></span>
      <div class="cm-at relative inline-flex after:pointer-events-none after:absolute after:-inset-[5px] after:rounded-xl after:border-2 after:border-[#7c3aed] after:opacity-0 [&.is-current]:after:opacity-100 motion-safe:[&.is-current]:after:animate-[cm-pulse_1.8s_ease-out_infinite]"><button class="inline-flex h-9 cursor-pointer items-center gap-2 rounded-[9px] border px-[13px] text-[.8125rem] leading-none font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#6d28d9] border-[#d9d2ec] bg-white text-[#1f1933] hover:bg-[#f4f1fb]" type="button" aria-label="Search"><svg class="size-4" aria-hidden="true"><use href="#i-search"/></svg></button>
        <div class="cm absolute top-[calc(100%+16px)] left-1/2 z-40 w-[290px] max-w-[calc(100vw-32px)] translate-x-[calc(-50%+var(--tt-nudge,0px))] rounded-[14px] bg-[#2e1065] px-4 pt-4 pb-3.5 text-left text-sm leading-normal text-[#f5f3ff] shadow-[0_24px_48px_-16px_rgba(46,16,101,.55)] motion-safe:animate-[cm-in_.2s_ease-out] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-[#a78bfa] before:absolute before:bottom-full before:left-[calc(50%-8px-var(--tt-nudge,0px))] before:border-8 before:border-t-0 before:border-transparent before:border-b-[#2e1065]" id="cm-1" role="dialog" aria-labelledby="cm-1-t" aria-describedby="cm-1-d" tabindex="-1" hidden>
          <small class="block text-xs font-semibold tracking-[.06em] text-[#c4b5fd] uppercase">Step 1 of 3</small><h3 class="mt-1 mb-1 text-base font-bold text-white" id="cm-1-t">Find anything</h3><p class="m-0 text-[#ddd6fe]" id="cm-1-d">Search clients, invoices and payments. Press <kbd class="rounded border border-[#6d4fc4] bg-[#3b1a80] px-[5px] py-px font-sans text-xs font-semibold text-white">⌘</kbd> <kbd class="rounded border border-[#6d4fc4] bg-[#3b1a80] px-[5px] py-px font-sans text-xs font-semibold text-white">K</kbd> from any page.</p>
          <div class="mt-3.5 flex items-center gap-2"><span class="mr-auto flex gap-[5px]" aria-hidden="true"><i class="h-[7px] w-[18px] rounded bg-[#ddd6fe]"></i><i class="size-[7px] rounded-full bg-[#6d4fc4]"></i><i class="size-[7px] rounded-full bg-[#6d4fc4]"></i></span><button class="h-8 cursor-pointer rounded-lg border-0 px-3 text-[.8125rem] leading-none font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#c4b5fd] bg-transparent text-[#ddd6fe] hover:bg-[#3b1a80] hover:text-white" type="button" data-step="skip">Skip tour</button><button class="h-8 cursor-pointer rounded-lg border-0 px-3 text-[.8125rem] leading-none font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#c4b5fd] bg-white text-[#4c1d95] hover:bg-[#ede9fe]" type="button" data-step="next">Next</button></div></div></div>
      <div class="cm-at relative inline-flex after:pointer-events-none after:absolute after:-inset-[5px] after:rounded-xl after:border-2 after:border-[#7c3aed] after:opacity-0 [&.is-current]:after:opacity-100 motion-safe:[&.is-current]:after:animate-[cm-pulse_1.8s_ease-out_infinite]"><button class="inline-flex h-9 cursor-pointer items-center gap-2 rounded-[9px] border px-[13px] text-[.8125rem] leading-none font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#6d28d9] border-[#6d28d9] bg-[#6d28d9] text-white hover:bg-[#5b21b6]" type="button"><svg class="size-4" aria-hidden="true"><use href="#i-plus"/></svg>New invoice</button>
        <div class="cm absolute top-[calc(100%+16px)] left-1/2 z-40 w-[290px] max-w-[calc(100vw-32px)] translate-x-[calc(-50%+var(--tt-nudge,0px))] rounded-[14px] bg-[#2e1065] px-4 pt-4 pb-3.5 text-left text-sm leading-normal text-[#f5f3ff] shadow-[0_24px_48px_-16px_rgba(46,16,101,.55)] motion-safe:animate-[cm-in_.2s_ease-out] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-[#a78bfa] before:absolute before:bottom-full before:left-[calc(50%-8px-var(--tt-nudge,0px))] before:border-8 before:border-t-0 before:border-transparent before:border-b-[#2e1065]" id="cm-2" role="dialog" aria-labelledby="cm-2-t" aria-describedby="cm-2-d" tabindex="-1" hidden>
          <small class="block text-xs font-semibold tracking-[.06em] text-[#c4b5fd] uppercase">Step 2 of 3</small><h3 class="mt-1 mb-1 text-base font-bold text-white" id="cm-2-t">Send your first invoice</h3><p class="m-0 text-[#ddd6fe]" id="cm-2-d">Add a client and line items. Ledgerly works out VAT and the due date for you.</p>
          <div class="mt-3.5 flex items-center gap-2"><span class="mr-auto flex gap-[5px]" aria-hidden="true"><i class="size-[7px] rounded-full bg-[#6d4fc4]"></i><i class="h-[7px] w-[18px] rounded bg-[#ddd6fe]"></i><i class="size-[7px] rounded-full bg-[#6d4fc4]"></i></span><button class="h-8 cursor-pointer rounded-lg border-0 px-3 text-[.8125rem] leading-none font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#c4b5fd] bg-transparent text-[#ddd6fe] hover:bg-[#3b1a80] hover:text-white" type="button" data-step="back">Back</button><button class="h-8 cursor-pointer rounded-lg border-0 px-3 text-[.8125rem] leading-none font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#c4b5fd] bg-white text-[#4c1d95] hover:bg-[#ede9fe]" type="button" data-step="next">Next</button></div></div></div>
      <div class="cm-at relative inline-flex after:pointer-events-none after:absolute after:-inset-[5px] after:rounded-xl after:border-2 after:border-[#7c3aed] after:opacity-0 [&.is-current]:after:opacity-100 motion-safe:[&.is-current]:after:animate-[cm-pulse_1.8s_ease-out_infinite]"><button class="grid size-9 cursor-pointer place-items-center rounded-full border-0 bg-[#fde68a] p-0 text-[.8rem] font-bold text-[#713f12] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#6d28d9]" type="button" aria-label="Account: Jonas Berg">JB</button>
        <div class="cm absolute top-[calc(100%+16px)] left-1/2 z-40 w-[290px] max-w-[calc(100vw-32px)] translate-x-[calc(-50%+var(--tt-nudge,0px))] rounded-[14px] bg-[#2e1065] px-4 pt-4 pb-3.5 text-left text-sm leading-normal text-[#f5f3ff] shadow-[0_24px_48px_-16px_rgba(46,16,101,.55)] motion-safe:animate-[cm-in_.2s_ease-out] focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-[#a78bfa] before:absolute before:bottom-full before:left-[calc(50%-8px-var(--tt-nudge,0px))] before:border-8 before:border-t-0 before:border-transparent before:border-b-[#2e1065]" id="cm-3" role="dialog" aria-labelledby="cm-3-t" aria-describedby="cm-3-d" tabindex="-1" hidden>
          <small class="block text-xs font-semibold tracking-[.06em] text-[#c4b5fd] uppercase">Step 3 of 3</small><h3 class="mt-1 mb-1 text-base font-bold text-white" id="cm-3-t">Add your bank details</h3><p class="m-0 text-[#ddd6fe]" id="cm-3-d">Put your logo and IBAN in Settings so every invoice is ready to send.</p>
          <div class="mt-3.5 flex items-center gap-2"><span class="mr-auto flex gap-[5px]" aria-hidden="true"><i class="size-[7px] rounded-full bg-[#6d4fc4]"></i><i class="size-[7px] rounded-full bg-[#6d4fc4]"></i><i class="h-[7px] w-[18px] rounded bg-[#ddd6fe]"></i></span><button class="h-8 cursor-pointer rounded-lg border-0 px-3 text-[.8125rem] leading-none font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#c4b5fd] bg-transparent text-[#ddd6fe] hover:bg-[#3b1a80] hover:text-white" type="button" data-step="back">Back</button><button class="h-8 cursor-pointer rounded-lg border-0 px-3 text-[.8125rem] leading-none font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#c4b5fd] bg-white text-[#4c1d95] hover:bg-[#ede9fe]" type="button" data-step="next">Done</button></div></div></div>
    </div>
    <div class="px-5 pt-[196px] pb-7 text-center text-sm text-[#5d5675]"><b class="block text-[.95rem] text-[#1f1933]">No invoices yet</b>Your sent and paid invoices will show up here.<br><button class="inline-flex h-9 cursor-pointer items-center gap-2 rounded-[9px] border px-[13px] text-[.8125rem] leading-none font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#6d28d9] mt-3 border-[#d9d2ec] bg-white text-[#1f1933] hover:bg-[#f4f1fb]" type="button" aria-controls="cm-1" aria-expanded="false" data-tour-start>Take the tour</button></div>
  </div>
</div></div>
CSS
@theme {
  --color-tt: #1f1933;
  --color-tt-ink: #f7f5ff;
}
/* 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) > *));
@keyframes cm-pulse { 0% { box-shadow: 0 0 0 0 rgba(124,58,237,.45) } 70%, 100% { box-shadow: 0 0 0 10px rgba(124,58,237,0) } }
@keyframes cm-in { from { opacity: 0; translate: calc(-50% + var(--tt-nudge, 0px)) 6px } }
JavaScript
(() => {
  // Keeps a tooltip or coach mark inside the window by nudging it sideways (the arrow stays on its target).
  const fit = tip => {
    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');
    });
  };

  // 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.querySelector('[role="tooltip"]')); };
  tts.forEach(t => { t.addEventListener('pointerenter', () => enter(t)); t.addEventListener('focusin', () => enter(t)); if (t.classList.contains('is-open')) fit(t.querySelector('[role="tooltip"]')); });
  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');
  });

  // Coach-mark tour: one step at a time. Next and Back move between steps and focus the new one; Skip, Done or Esc end the tour.
  const tour = document.querySelector('.tour');
  const steps = tour ? [...tour.querySelectorAll('.cm')] : [];
  const start = document.querySelector('[data-tour-start]');
  let cur = -1;
  const go = (i, focus = true) => {
    steps.forEach((s, n) => { s.hidden = n !== i; s.parentElement.classList.toggle('is-current', n === i); });
    cur = i;
    if (start) start.setAttribute('aria-expanded', i >= 0);
    if (i >= 0) { fit(steps[i]); if (focus) steps[i].focus(); }
  };
  const end = () => { const inside = cur >= 0 && steps[cur].contains(document.activeElement); go(-1); if (inside && start) start.focus(); };
  if (tour) tour.addEventListener('click', e => {
    const b = e.target.closest('[data-step]'); if (!b) return;
    if (b.dataset.step === 'next') cur < steps.length - 1 ? go(cur + 1) : end();
    else if (b.dataset.step === 'back') go(cur - 1);
    else end();
  });
  if (start) start.addEventListener('click', () => go(0));

  document.addEventListener('keydown', e => {
    if (e.key !== 'Escape') return;
    if (cur >= 0) { end(); 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

Tooltips that teach. Tooltips with keyboard shortcuts gives an editing toolbar of Undo, Redo, Find in document and Add comment buttons, each with a dark tooltip that shows the command name and its keys as raised keycaps, such as Option Command M. The button is named by the label part of the tooltip and described by the keys part, whose symbols carry visually hidden words like Command and Shift, and aria-keyshortcuts lists the shortcut. Onboarding coach marks is a three step tour of an invoicing app called Ledgerly: Find anything points at search, Send your first invoice points at the New invoice button, and Add your bank details points at the account menu. Each step is a small non-modal dialog with a step count, progress dots and Skip, Back, Next or Done buttons, and a pulsing ring marks the control it explains. Next and Back move focus into the new step, and Skip, Done or Esc end the tour, returning focus to the Take the tour button if it was inside the step. The Bootstrap version builds the shortcut tooltips with Bootstrap Tooltip and each tour step with Bootstrap Popover.

What’s included

  • Shortcut keys drawn as keycaps inside the tooltip
  • Keys described in words for screen readers, plus aria-keyshortcuts
  • Three-step coach mark tour with progress dots
  • Pulsing ring on the control each step explains
  • Focus moves with the tour and returns when it ends
  • Bootstrap Tooltip and Popover in the Bootstrap version

Accessibility

Shortcut buttons take their name from the tooltip label and their description from the keys, so screen readers hear Undo, then Command Z. Each coach mark is a dialog labelled by its heading and described by its text; Next and Back focus the new step, and Esc, Skip or Done close the tour and, when focus was in the step, return it to the start button. Every control has a visible focus outline, and the pulse and entrance animation stop under prefers-reduced-motion.

Customise it

The tooltip uses --tt-bg and --tt-ink and the keycaps use the .keys kbd colours; the coach mark colours are on .cm. In Bootstrap edit the --bs-tooltip-* variables on .tooltip.tt and the --bs-popover-* variables on .popover.cm. In Tailwind change the theme colours and the hex utilities on the coach marks.