Default

<button class="btn" data-variant="outline" data-tooltip-trigger="my-tip">Hover me</button>
<div class="tooltip" id="my-tip" popover="hint" role="tooltip">Add to library</div>

Side

Use data-side on the tooltip to control placement.

<!-- Top (default) -->
<button data-tooltip-trigger="tip-top">Top</button>
<div class="tooltip" id="tip-top" popover="hint" role="tooltip">Top tooltip</div>

<!-- Bottom -->
<button data-tooltip-trigger="tip-bottom">Bottom</button>
<div class="tooltip" id="tip-bottom" popover="hint" role="tooltip" data-side="bottom">Bottom tooltip</div>

<!-- Left -->
<button data-tooltip-trigger="tip-left">Left</button>
<div class="tooltip" id="tip-left" popover="hint" role="tooltip" data-side="left">Left tooltip</div>

<!-- Right -->
<button data-tooltip-trigger="tip-right">Right</button>
<div class="tooltip" id="tip-right" popover="hint" role="tooltip" data-side="right">Right tooltip</div>

With Arrow

Add <div data-arrow></div> inside the tooltip for a connecting caret.

<button data-tooltip-trigger="my-tip">Hover me</button>
<div class="tooltip" id="my-tip" popover="hint" role="tooltip">
  Tooltip text
  <div data-arrow></div>
</div>

With Keyboard Shortcut

<button class="btn" data-variant="outline" data-tooltip-trigger="tip-kbd">Save</button>
<div class="tooltip" id="tip-kbd" popover="hint" role="tooltip"
     style="display:flex;align-items:center;gap:0.5rem;" data-side="bottom">
  Save document
  <kbd style="font-size:0.625rem;padding:0.125rem 0.375rem;border-radius:var(--radius-sm);
    background:oklch(from var(--primary-foreground) l c h / 0.15);
    color:var(--primary-foreground);font-family:var(--font-mono);">⌘S</kbd>
</div>

Alignment

Use data-align to control tooltip alignment relative to the trigger.

<!-- Start-aligned -->
<div class="tooltip" id="tip" popover="hint" role="tooltip" data-align="start">...</div>

<!-- End-aligned -->
<div class="tooltip" id="tip" popover="hint" role="tooltip" data-align="end">...</div>

Disabled Button

Wrap a disabled button in a <span> with the trigger attribute, since disabled elements don't fire mouse events.

<span data-tooltip-trigger="tip-disabled" style="cursor:not-allowed;">
  <button class="btn" disabled style="pointer-events:none;">Disabled</button>
</span>
<div class="tooltip" id="tip-disabled" popover="hint" role="tooltip">
  This action is unavailable
</div>

Custom Delay

Use data-delay on the trigger to override the default 700 ms open delay.

<!-- Instant -->
<button data-tooltip-trigger="tip" data-delay="0">Instant</button>

<!-- Custom delay -->
<button data-tooltip-trigger="tip" data-delay="200">Fast</button>

Group Behavior

Once any tooltip opens, subsequent tooltips in the page skip the delay and appear instantly. After 400 ms with no tooltip visible, the delay resets. Hover across the buttons below to see the effect.

<!-- Once any tooltip is open, the rest appear instantly -->
<button data-tooltip-trigger="tip-bold">B</button>
<div class="tooltip" id="tip-bold" popover="hint" role="tooltip">Bold</div>

<button data-tooltip-trigger="tip-italic">I</button>
<div class="tooltip" id="tip-italic" popover="hint" role="tooltip">Italic</div>

CSS view file

Uses position-area for placement, position-try-fallbacks for collision avoidance, and @starting-style for direction-aware animations.

@layer components {
  .tooltip {
    position: fixed;
    inset: auto;
    margin: 0;
    border: none;
    border-radius: var(--radius-md);
    background-color: var(--primary);
    color: var(--primary-foreground);
    padding: 0.375rem 0.75rem;
    font-size: 0.75rem;
    line-height: 1.4;
    box-shadow: var(--shadow-sm);
    pointer-events: none;
    max-width: 16rem;
    width: max-content;
    opacity: 0;
    transition: opacity 150ms ease, translate 150ms ease, display 150ms allow-discrete;

    /* -- Default: top center -- */
    position-area: top;
    margin-bottom: 6px;
    translate: 0 2px;
    position-try-fallbacks: flip-block, flip-inline;

    &:popover-open { opacity: 1; translate: 0 0; }

    /* ----- Side variants ----- */
    &[data-side="bottom"] {
      position-area: bottom;
      margin-bottom: 0;
      margin-top: 6px;
      translate: 0 -2px;
      &:popover-open { translate: 0 0; }
    }

    &[data-side="left"] {
      position-area: left;
      margin-bottom: 0;
      margin-right: 6px;
      translate: 2px 0;
      &:popover-open { translate: 0 0; }
    }

    &[data-side="right"] {
      position-area: right;
      margin-bottom: 0;
      margin-left: 6px;
      translate: -2px 0;
      &:popover-open { translate: 0 0; }
    }

    /* ----- Align variants (combined with side) ----- */
    &[data-align="start"] {
      &:not([data-side="left"]):not([data-side="right"]) { position-area: top left; }
      &[data-side="bottom"] { position-area: bottom left; }
      &[data-side="left"] { position-area: left top; }
      &[data-side="right"] { position-area: right top; }
    }

    &[data-align="end"] {
      &:not([data-side="left"]):not([data-side="right"]) { position-area: top right; }
      &[data-side="bottom"] { position-area: bottom right; }
      &[data-side="left"] { position-area: left bottom; }
      &[data-side="right"] { position-area: right bottom; }
    }

    /* ----- Arrow ----- */
    & [data-arrow] {
      position: absolute;
      width: 8px;
      height: 8px;
      background: inherit;
      rotate: 45deg;
      border: none;
    }

    /* Arrow placement — default (top side): arrow at bottom center */
    &:not([data-side]), &[data-side="top"] {
      & [data-arrow] { bottom: -4px; left: 50%; margin-left: -4px; }
    }
    &[data-side="bottom"] {
      & [data-arrow] { top: -4px; left: 50%; margin-left: -4px; }
    }
    &[data-side="left"] {
      & [data-arrow] { right: -4px; top: 50%; margin-top: -4px; }
    }
    &[data-side="right"] {
      & [data-arrow] { left: -4px; top: 50%; margin-top: -4px; }
    }
  }

  @starting-style {
    .tooltip:popover-open { opacity: 0; translate: 0 2px; }
    .tooltip[data-side="bottom"]:popover-open { opacity: 0; translate: 0 -2px; }
    .tooltip[data-side="left"]:popover-open { opacity: 0; translate: 2px 0; }
    .tooltip[data-side="right"]:popover-open { opacity: 0; translate: -2px 0; }
  }
}

JavaScript view file

Delay, group behavior, ARIA wiring, and scroll dismiss.

// -- Tooltip --------------------------------------------------
// Popover API tooltips with delay, group behavior, ARIA wiring,
// CSS anchor positioning, and scroll dismiss.

const DELAY_DEFAULT = 700;      // ms before first tooltip opens
const CLOSE_DELAY_DEFAULT = 0;  // ms before tooltip closes
const GROUP_TIMEOUT = 400;      // ms after last tooltip hides before delay resets

let groupOpen = false;       // true while any tooltip is visible
let groupTimer = null;       // timeout to reset groupOpen

function markGroupOpen() {
  groupOpen = true;
  clearTimeout(groupTimer);
}

function scheduleGroupReset() {
  clearTimeout(groupTimer);
  groupTimer = setTimeout(() => { groupOpen = false; }, GROUP_TIMEOUT);
}

function init() {
document.querySelectorAll('[data-tooltip-trigger]:not([data-init])').forEach((trigger) => {
  trigger.dataset.init = '';
  const tip = document.getElementById(trigger.dataset.tooltipTrigger);
  if (!tip) return;

  // CSS anchor positioning — unique name per trigger-tooltip pair
  const anchorId = `--tooltip-${tip.id}`;
  trigger.style.anchorName = anchorId;
  tip.style.positionAnchor = anchorId;

  // ARIA — link trigger to tooltip
  trigger.setAttribute('aria-describedby', tip.id);

  const delay = Number(trigger.dataset.delay ?? DELAY_DEFAULT);
  const closeDelay = Number(trigger.dataset.closeDelay ?? CLOSE_DELAY_DEFAULT);

  let openTimer = null;
  let closeTimer = null;

  function show() {
    clearTimeout(closeTimer);
    clearTimeout(openTimer);
    const wait = groupOpen ? 0 : delay;
    openTimer = setTimeout(() => {
      try { tip.showPopover(); } catch (e) { /* already open */ }
      markGroupOpen();
    }, wait);
  }

  function hide() {
    clearTimeout(openTimer);
    clearTimeout(closeTimer);
    closeTimer = setTimeout(() => {
      try { tip.hidePopover(); } catch (e) { /* already closed */ }
      scheduleGroupReset();
    }, closeDelay);
  }

  trigger.addEventListener('mouseenter', show);
  trigger.addEventListener('mouseleave', hide);
  trigger.addEventListener('focus', show);
  trigger.addEventListener('blur', hide);
});
}

init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });

// -- Scroll dismiss -------------------------------------------
// Hide any open tooltip when the page scrolls.
if (!document.__tooltipScrollInit) {
  document.__tooltipScrollInit = true;
  document.addEventListener('scroll', () => {
    document.querySelectorAll('.tooltip:popover-open').forEach((tip) => {
      try { tip.hidePopover(); } catch (e) {}
    });
  }, { passive: true, capture: true });
}