component

Tooltip

A small, non-interactive label that appears on hover or focus to describe a control. Uses role="tooltip" and wires the trigger to the tooltip via aria-describedby so assistive technologies announce it as a description of the control.

Tooltips are not for interactive content — buttons, links, or forms inside a tooltip will be inaccessible to keyboard users and unreachable by pointer since the tooltip closes on mouseleave. Use Popover for interactive overlays.

Default

Opens after a short hover delay (300ms) or when the trigger receives focus. Dismisses on mouseleave, blur, or ESC.

---
import Tooltip from "../tooltip.astro";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
---

<div class={stack({ gap: "md", direction: "row", align: "center" })}>
  <Tooltip id="astro-demo-tooltip-default" content="Save the document (⌘S)">
    <button class={css({ px: "sm", py: "xs" })}>Save</button>
  </Tooltip>
</div>

Placement

Use the placement prop to control where the tooltip appears relative to its trigger. Floating UI flips or shifts automatically to keep the tooltip inside the viewport.

---
import Tooltip from "../tooltip.astro";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";

const placements = ["top", "right", "bottom", "left"] as const;
---

<div class={stack({ gap: "md", direction: "row", align: "center" })}>
  {
    placements.map((placement) => (
      <Tooltip
        id={`astro-tt-placement-${placement}`}
        content={`Tooltip on ${placement}`}
        placement={placement}
      >
        <button class={css({ px: "sm", py: "xs" })}>{placement}</button>
      </Tooltip>
    ))
  }
</div>

Wrap

By default the tooltip panel sizes to its text but caps at 20rem, wrapping longer content to multiple lines. Set wrap={false} to size the panel to its text on a single line (white-space: nowrap, no max width) — ideal for short labels that must not break, like a keyboard shortcut or a file path.

---
import Tooltip from "../tooltip.astro";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";

const long =
  "This is a long tooltip that would otherwise wrap across several lines once it hits the 20rem cap.";
const btn = css({ px: "sm", py: "xs" });
---

<div class={stack({ gap: "lg", direction: "row", align: "center" })}>
  <Tooltip id="astro-demo-tooltip-wrap-on" content={long}>
    <button class={btn}>wrap (default)</button>
  </Tooltip>

  <Tooltip id="astro-demo-tooltip-wrap-off" content={long} wrap={false}>
    <button class={btn}>wrap=false</button>
  </Tooltip>
</div>

Group

All tooltips on the page share a single DOM element and a single open-delay timer. The first trigger you hover pays the full openDelay (300ms); hovering a neighbour while a tooltip is open — or within 500ms after it closes — skips the delay and the tooltip slides from the previous trigger’s anchor to the new one (FLIP animation). This matches the macOS toolbar feel and keeps dense UIs feeling snappy without sacrificing the “first hover is deliberate” guarantee.

---
import Tooltip from "../tooltip.astro";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";

const actions = [
  { id: "bold", label: "Bold (⌘B)", icon: "B" },
  { id: "italic", label: "Italic (⌘I)", icon: "I" },
  { id: "underline", label: "Underline (⌘U)", icon: "U" },
  { id: "strike", label: "Strikethrough", icon: "S" },
  { id: "link", label: "Insert link (⌘K)", icon: "🔗" },
];

const buttonClass = css({
  px: "sm",
  py: "xs",
  minWidth: "2.25rem",
  borderRadius: "sm",
  fontFamily: "mono",
  fontSize: "sm",
});
---

<div class={stack({ gap: "2xs", direction: "row", align: "center" })}>
  {
    actions.map((action) => (
      <Tooltip id={`astro-tt-group-${action.id}`} content={action.label}>
        <button class={buttonClass} aria-label={action.label}>
          {action.icon}
        </button>
      </Tooltip>
    ))
  }
</div>

Inside a modal drawer

Tooltips work inside modal drawers, dialogs, and popovers — even though those use the browser top layer via <dialog>.showModal(). The shared tooltip element is promoted into the top layer via the native Popover API (popover="manual"), so it always paints above whatever modal it’s triggered from. Browsers without Popover API support fall back to a body-appended element at z-index: 9999.

Edit document

Tooltips inside a modal drawer paint above the backdrop because the shared tooltip element is promoted into the browser top layer.

---
import Tooltip from "../tooltip.astro";
import Dialog from "@pindoba/astro-dialog";
import Button from "@pindoba/astro-button";
import { Redo2, Save, Trash2, Undo2 } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<Dialog id="astro-demo-tooltip-in-drawer" title="Edit document" drawer="right">
  <div class={stack({ gap: "md" })}>
    <p>
      Tooltips inside a modal drawer paint above the backdrop because the shared
      tooltip element is promoted into the browser top layer.
    </p>

    <div class={stack({ gap: "xs", direction: "row" })}>
      <Tooltip id="astro-demo-tooltip-in-drawer-save" content="Save (⌘S)">
        <Button emphasis="ghost" shape="square" aria-label="Save">
          <Save width={18} height={18} />
        </Button>
      </Tooltip>
      <Tooltip id="astro-demo-tooltip-in-drawer-undo" content="Undo (⌘Z)">
        <Button emphasis="ghost" shape="square" aria-label="Undo">
          <Undo2 width={18} height={18} />
        </Button>
      </Tooltip>
      <Tooltip id="astro-demo-tooltip-in-drawer-redo" content="Redo (⇧⌘Z)">
        <Button emphasis="ghost" shape="square" aria-label="Redo">
          <Redo2 width={18} height={18} />
        </Button>
      </Tooltip>
      <Tooltip
        id="astro-demo-tooltip-in-drawer-delete"
        content="Delete (⌫)"
        placement="bottom"
      >
        <Button
          emphasis="ghost"
          shape="square"
          feedback="danger"
          aria-label="Delete"
        >
          <Trash2 width={18} height={18} />
        </Button>
      </Tooltip>
    </div>
  </div>
</Dialog>

<Button id="astro-demo-tooltip-in-drawer-trigger">Open drawer</Button>

<script>
  document
    .getElementById("astro-demo-tooltip-in-drawer-trigger")
    ?.addEventListener("click", () => {
      window.__PindobaDialogManager?.open("astro-demo-tooltip-in-drawer");
    });
</script>

Inside a popover

Same mechanism — useful for icon toolbars inside popovers where the icons themselves need labels.

Text formatting

---
import Tooltip from "../tooltip.astro";
import Popover from "@pindoba/astro-popover";
import Button from "@pindoba/astro-button";
import { Bold, Italic, Underline } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md", direction: "column", align: "start" })}>
  <Popover id="astro-demo-tooltip-in-popover" title="Text formatting">
    <div class={stack({ gap: "xs", direction: "row" })}>
      <Tooltip
        id="astro-demo-tooltip-in-popover-bold"
        content="Bold (⌘B)"
        triggerStrategy="hover"
      >
        <Button emphasis="ghost" shape="square" size="sm" aria-label="Bold">
          <Bold width={16} height={16} />
        </Button>
      </Tooltip>
      <Tooltip
        id="astro-demo-tooltip-in-popover-italic"
        content="Italic (⌘I)"
        triggerStrategy="hover"
      >
        <Button emphasis="ghost" shape="square" size="sm" aria-label="Italic">
          <Italic width={16} height={16} />
        </Button>
      </Tooltip>
      <Tooltip
        id="astro-demo-tooltip-in-popover-underline"
        content="Underline (⌘U)"
        triggerStrategy="hover"
      >
        <Button
          emphasis="ghost"
          shape="square"
          size="sm"
          aria-label="Underline"
        >
          <Underline width={16} height={16} />
        </Button>
      </Tooltip>
    </div>
  </Popover>

  <Button id="astro-demo-tooltip-in-popover-trigger">Open popover</Button>
</div>

<script>
  import { usePopover } from "@pindoba/astro-popover/use-popover";

  const trigger = document.getElementById(
    "astro-demo-tooltip-in-popover-trigger",
  );
  usePopover({ id: "astro-demo-tooltip-in-popover", trigger });
</script>

Props

props · 13 total
proptypedefaultreqdescription
contentstringSnippetundefinedTooltip content. Can be a plain string or a Svelte snippet for rich markup.
idstringauto-generated UUIDUnique identifier used for the tooltip element and wired to the trigger via aria-describedby.
placement"top""top-start""top-end""bottom""bottom-start""bottom-end""left""left-start""left-end""right""right-start""right-end""top"Preferred placement of the tooltip relative to the trigger. Floating UI flips or shifts as needed.
triggerStrategy"hover""focus""hover-focus""manual""hover-focus"How the trigger opens the tooltip. 'hover' opens on mouseenter. 'focus' opens on focus. 'hover-focus' combines both (recommended for accessibility). 'manual' leaves event wiring to the consumer.
openDelaynumber300Delay in ms before a hover trigger opens the tooltip. Ignored by focus/manual.
closeDelaynumber100Delay in ms before a hover trigger closes the tooltip. Ignored by focus/manual.
offsetnumber6Distance in pixels between trigger and tooltip.
openWhenstringundefinedCSS selector gating hover/focus opens: the tooltip only opens while the trigger, or one of its ancestors, matches it. Re-checked on every interaction, so it follows live DOM changes with no re-render…

openWhen

CSS selector gating hover/focus opens: the tooltip only opens while the trigger, or one of its ancestors, matches it. Re-checked on every interaction, so it follows live DOM changes with no re-render — a collapsible navigation rail passes '[data-component="navigation"][data-compact]' so labels only appear on hover while collapsed. Omit for a tooltip that always opens.

Typestring
Defaultundefined
RequiredNo
anchorstringundefinedCSS selector for the element the tooltip positions against, when that is not the element it listens on. Hover/focus and aria-describedby stay on the trigger; only the floating reference moves. Use it…

anchor

CSS selector for the element the tooltip positions against, when that is not the element it listens on. Hover/focus and aria-describedby stay on the trigger; only the floating reference moves. Use it when the trigger opens something else on the same interaction — a collapsed navigation rail's row opens a flyout panel, and a tooltip anchored to the row would land on top of it. Resolved when the tooltip opens; an anchor with no layout boxes (a closed dialog) falls back to the trigger.

Typestring
Defaultundefined
RequiredNo
size"sm""md""lg""md"Visual size variant.
wrapbooleantrueWhether long content wraps. true caps the panel at 20rem and wraps to multiple lines; false sizes the panel to its text on a single line (no max width, white-space: nowrap).
openbooleanfalseControls the open/closed state. Bindable in Svelte — useful for the 'manual' strategy.
childrenSnippet<[{ 'data-tooltip-trigger': true }]>undefinedSnippet that renders the trigger element. The tooltip wires hover/focus listeners to the node you attach the props to.
  • Components
  • Blocks