Component

Hotspot Tooltip

Onboarding hotspot beacon that opens a multi-step guided tour tooltip. Click the pulsing beacon to start; Next/Done walk the steps, Skip or Escape dismiss it.

  • Component
  • Tailwind
  • HTML
  • MIT

Preview

Live component

Interactive preview
Open preview
9:41 100%
devsnips.dev/library/tailwind/components/tooltips/hotspot-tooltip/
fluid · no fixed width 100%
No dependencies Reduced-motion

Install

Add to your project

terminal
npx devsnips add Tailwind/Components/Tooltips/hotspot-tooltip
registry path: Tailwind/Components/Tooltips/hotspot-tooltip

Source

Implementation

code.html
<!--
Snippet Name: Hotspot Tooltip
Description: Onboarding hotspot beacon with a multi-step tour tooltip. Click to open; Next/Skip walk the steps; Escape or outside click closes.
Author: DevSnips Contributors
Usage Example: Place the data-hotspot-tooltip wrapper next to the feature you announce, then edit the STEPS array in the script.
-->
<div class="relative inline-flex" data-hotspot-tooltip="hotspot-tooltip">
  <button
    type="button"
    class="hs-trigger relative inline-flex h-8 w-8 items-center justify-center rounded-full bg-[#0a0a0a] ring-1 ring-white/10 transition-colors duration-150 hover:bg-[#111111] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#50e3c2] focus-visible:ring-offset-2 motion-reduce:transition-none"
    aria-expanded="false"
    aria-controls="hotspot-tip"
    aria-label="What's new: start the tour"
  >
    <span aria-hidden="true" class="absolute inset-0 rounded-full bg-[#50e3c2]/40 motion-safe:animate-ping motion-reduce:animate-none"></span>
    <span aria-hidden="true" class="relative h-2 w-2 rounded-full bg-[#50e3c2] shadow-[0_0_8px_rgba(80,227,194,0.9)]"></span>
  </button>

  <div
    id="hotspot-tip"
    role="dialog"
    aria-labelledby="hotspot-tip-title"
    class="absolute bottom-full left-1/2 mb-2.5 hidden w-72 max-w-[calc(100vw-2rem)] -translate-x-1/2 opacity-0 scale-95 transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-none"
  >
    <div class="rounded-lg border border-white/10 bg-[#0a0a0a] shadow-[0_16px_48px_-12px_rgba(0,0,0,0.55)]">
      <div class="flex items-start justify-between gap-3 p-4 pb-0">
        <span class="hs-eyebrow inline-flex items-center gap-1.5 pt-0.5">
          <span aria-hidden="true" class="h-1.5 w-1.5 rounded-full bg-[#50e3c2]"></span>
          <span class="text-[11px] font-medium uppercase tracking-widest text-white/40">New feature</span>
        </span>
        <button
          type="button"
          class="hs-close -mr-1 -mt-0.5 rounded-md p-1 text-white/40 transition-colors duration-150 hover:bg-white/5 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-[#50e3c2] motion-reduce:transition-none"
          aria-label="Close tour"
        >
          <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
        </button>
      </div>

      <div class="p-4 pb-0">
        <h3 id="hotspot-tip-title" class="hs-title text-sm font-semibold tracking-tight text-white"></h3>
        <p class="hs-desc mt-1.5 text-[13px] leading-relaxed text-white/60"></p>
        <p class="hs-kbd-row mt-3 hidden items-center gap-1 text-[12px] text-white/40">
          Try it:
          <kbd class="hs-kbd ml-0.5 rounded border border-white/10 bg-white/5 px-1.5 py-0.5 font-sans text-[11px] font-medium text-white/70"></kbd>
        </p>
      </div>

      <div class="flex items-center gap-3 p-4 pt-3">
        <div class="hs-dots flex min-w-0 flex-1 items-center gap-1" aria-hidden="true"></div>
        <span class="hs-counter text-[11px] tabular-nums text-white/40"></span>
        <button
          type="button"
          class="hs-skip rounded-md px-2 py-1.5 text-xs font-medium text-white/50 transition-colors duration-150 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-[#50e3c2] motion-reduce:transition-none"
        >Skip</button>
        <button
          type="button"
          class="hs-next rounded-md bg-[#50e3c2] px-3 py-1.5 text-xs font-semibold text-[#0a0a0a] transition-colors duration-150 hover:bg-[#7cf0d5] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#50e3c2] focus-visible:ring-offset-2 focus-visible:ring-offset-[#0a0a0a] motion-reduce:transition-none"
        >Next</button>
      </div>
    </div>
    <span aria-hidden="true" class="absolute -bottom-1.5 left-1/2 h-3 w-3 -translate-x-1/2 rotate-45 border-b border-r border-white/10 bg-[#0a0a0a]"></span>
  </div>

  <script>
    (function () {
      var root = document.currentScript.closest('[data-hotspot-tooltip="hotspot-tooltip"]');
      if (!root) return;

      var STEPS = [
        {
          eyebrow: 'New feature',
          title: 'Command palette',
          desc: 'Jump to any snippet, page, or action without touching the mouse. One keystroke, zero navigation.',
          keys: ['\u2318', 'K']
        },
        {
          eyebrow: 'Saved views',
          title: 'Pin your go-to snippets',
          desc: 'Star the components you reach for daily and they stay pinned in the sidebar, synced across your devices.',
          keys: null
        },
        {
          eyebrow: 'Team workspace',
          title: 'Share a curated library',
          desc: 'Invite your team to one source of truth for snippets, with role-based access and usage insights.',
          keys: null
        }
      ];

      var trigger = root.querySelector('.hs-trigger');
      var tip = root.querySelector('#hotspot-tip');
      var eyebrowEl = tip.querySelector('.hs-eyebrow span:last-child');
      var titleEl = tip.querySelector('.hs-title');
      var descEl = tip.querySelector('.hs-desc');
      var kbdRow = tip.querySelector('.hs-kbd-row');
      var kbdEl = tip.querySelector('.hs-kbd');
      var dotsEl = tip.querySelector('.hs-dots');
      var counterEl = tip.querySelector('.hs-counter');
      var skipBtn = tip.querySelector('.hs-skip');
      var nextBtn = tip.querySelector('.hs-next');
      var closeBtn = tip.querySelector('.hs-close');

      var current = 0;
      var open = false;
      var hideTimer = null;

      function renderStep() {
        var s = STEPS[current];
        eyebrowEl.textContent = s.eyebrow;
        titleEl.textContent = s.title;
        descEl.textContent = s.desc;
        if (s.keys) {
          kbdEl.textContent = s.keys.join(' ');
          kbdRow.classList.remove('hidden');
          kbdRow.classList.add('flex');
        } else {
          kbdRow.classList.add('hidden');
          kbdRow.classList.remove('flex');
        }
        dotsEl.innerHTML = STEPS.map(function (_, i) {
          var active = i <= current;
          return '<span class="h-1 flex-1 rounded-full ' + (active ? 'bg-[#50e3c2]' : 'bg-white/10') + '"></span>';
        }).join('');
        counterEl.textContent = (current + 1) + ' / ' + STEPS.length;
        nextBtn.textContent = current === STEPS.length - 1 ? 'Done' : 'Next';
      }

      function setOpen(isOpen) {
        open = isOpen;
        trigger.setAttribute('aria-expanded', String(isOpen));
        if (isOpen) {
          renderStep();
          if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
          tip.classList.remove('hidden');
          requestAnimationFrame(function () {
            tip.classList.remove('opacity-0', 'scale-95');
          });
          document.addEventListener('pointerdown', onOutside, true);
          document.addEventListener('keydown', onKeydown, true);
        } else {
          tip.classList.add('opacity-0', 'scale-95');
          hideTimer = window.setTimeout(function () { tip.classList.add('hidden'); }, 160);
          document.removeEventListener('pointerdown', onOutside, true);
          document.removeEventListener('keydown', onKeydown, true);
        }
      }

      function onOutside(e) {
        if (!root.contains(e.target)) setOpen(false);
      }

      function onKeydown(e) {
        if (e.key === 'Escape') {
          setOpen(false);
          trigger.focus();
        }
      }

      trigger.addEventListener('click', function () { setOpen(!open); });
      closeBtn.addEventListener('click', function () { setOpen(false); });
      skipBtn.addEventListener('click', function () { current = 0; setOpen(false); });
      nextBtn.addEventListener('click', function () {
        if (current < STEPS.length - 1) {
          current += 1;
          renderStep();
        } else {
          current = 0;
          setOpen(false);
        }
      });
    })();
  </script>
</div>
174 lines UTF-8 · LF · Spaces: 2

AI-ready

Available to your agent.

This component is reachable through the DevSnips CLI, MCP server and Skills integrations — one registry, one resource path.

Resource path

devsnips://components/tooltips/hotspot-tooltip

Continue browsing

View all