Hotspot

Pulsing indicator inviting interaction.

html

<div class="hotspot-stage">
  <button type="button" class="hotspot hotspot--pulse" data-position="top-right" style="--accent:#E8A020;--size:12px;" aria-label="New: Bulk export">
    <span class="hotspot__tooltip" role="tooltip">New: Bulk export</span>
  </button>
</div>
<style>
.hotspot-stage { position: relative; min-height: 190px; font: 16px/1.5 system-ui, sans-serif; background: #FAF7F2; border-radius: 12px; }
.hotspot { position: absolute; width: var(--size); height: var(--size); border-radius: 50%; background: var(--accent); border: none; padding: 0; cursor: pointer; }
.hotspot[data-position="top-left"] { top: 20px; left: 20px; }
.hotspot[data-position="top-right"] { top: 20px; right: 20px; }
.hotspot[data-position="bottom-left"] { bottom: 20px; left: 20px; }
.hotspot[data-position="bottom-right"] { bottom: 20px; right: 20px; }
.hotspot--pulse::after { content: ""; position: absolute; inset: 0; border-radius: 50%; background: var(--accent); opacity: .6; }
@media (prefers-reduced-motion: no-preference) {
.hotspot--pulse::after { animation: hotspot-pulse .22s ease-out infinite; }
}
@keyframes hotspot-pulse { to { transform: scale(2.6); opacity: 0; } }
.hotspot__tooltip { position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translateX(-50%); background: #241430; color: #FAF7F2; font-size: 12.5px; padding: 6px 10px; border-radius: 6px; white-space: nowrap; opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: no-preference) {
.hotspot__tooltip { transition: opacity .15s ease; }
}
.hotspot:hover .hotspot__tooltip, .hotspot:focus-visible .hotspot__tooltip { opacity: 1; }
.hotspot:focus-visible { outline: 2px solid #241430; outline-offset: 3px; }
</style>

tailwind

<div class="relative min-h-[190px] font-sans bg-[#FAF7F2] rounded-xl">
  <button type="button" class="hotspot-tw hotspot-tw--pulse absolute rounded-full border-0 p-0 cursor-pointer" data-position="top-right" style="background:#E8A020;width:12px;height:12px;" aria-label="New: Bulk export">
    <span class="hotspot-tw__tooltip absolute bottom-full left-1/2 -translate-x-1/2 mb-2.5 bg-[#241430] text-[#FAF7F2] text-xs px-2.5 py-1.5 rounded-md whitespace-nowrap opacity-0 pointer-events-none motion-safe:transition-opacity" role="tooltip">New: Bulk export</span>
  </button>
</div>
<style>
.hotspot-tw[data-position="top-left"] { top: 20px; left: 20px; }
.hotspot-tw[data-position="top-right"] { top: 20px; right: 20px; }
.hotspot-tw[data-position="bottom-left"] { bottom: 20px; left: 20px; }
.hotspot-tw[data-position="bottom-right"] { bottom: 20px; right: 20px; }
.hotspot-tw--pulse::after { content: ""; position: absolute; inset: 0; border-radius: 50%; background: inherit; opacity: .6; }
@media (prefers-reduced-motion: no-preference) {
.hotspot-tw--pulse::after { animation: hotspot-tw-pulse .22s ease-out infinite; }
}
@keyframes hotspot-tw-pulse { to { transform: scale(2.6); opacity: 0; } }
.hotspot-tw:hover .hotspot-tw__tooltip, .hotspot-tw:focus-visible .hotspot-tw__tooltip { opacity: 1; }
</style>

react

export function Hotspot({
  label = "New: Bulk export",
  position = "top-right",
  size = 12,
  accent = "#E8A020",
  pulse = true,
}) {
  const corner = {
    "top-left": { top: 20, left: 20 },
    "top-right": { top: 20, right: 20 },
    "bottom-left": { bottom: 20, left: 20 },
    "bottom-right": { bottom: 20, right: 20 },
  }[position] ?? {};
  return (
    <div style={{ position: "relative", minHeight: 190, fontFamily: "system-ui, sans-serif", background: "#FAF7F2", borderRadius: 12 }}>
      <button type="button" aria-label={label} className="hotspot-react" style={{ position: "absolute", width: size, height: size, borderRadius: "50%", background: accent, border: "none", padding: 0, cursor: "pointer", ...corner }}>
        {pulse && (
          <span className="hotspot-react-pulsering" style={{ position: "absolute", inset: 0, borderRadius: "50%", background: accent, opacity: 0.6 }} />
        )}
        <span role="tooltip" className="hotspot-react-tip" style={{ position: "absolute", bottom: "calc(100% + 10px)", left: "50%", transform: "translateX(-50%)", background: "#241430", color: "#FAF7F2", fontSize: 12.5, padding: "6px 10px", borderRadius: 6, whiteSpace: "nowrap", opacity: 0, pointerEvents: "none" }}>{label}</span>
      </button>
      <style>{`
        @media (prefers-reduced-motion: no-preference) {
          .hotspot-react-pulsering { animation: hotspot-react-pulse 1.6s ease-out infinite; }
          @keyframes hotspot-react-pulse { to { transform: scale(2.6); opacity: 0; } }
        }
        .hotspot-react:hover .hotspot-react-tip, .hotspot-react:focus-visible .hotspot-react-tip { opacity: 1; }
      `}</style>
    </div>
  );
}