component
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.
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>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>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>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>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.
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>Same mechanism — useful for icon toolbars inside popovers where the icons themselves need labels.
---
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>| prop | type | default | req | description |
|---|---|---|---|---|
| content | stringSnippet | undefined | Tooltip content. Can be a plain string or a Svelte snippet for rich markup. | |
| id | string | auto-generated UUID | Unique 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. | |
| openDelay | number | 300 | Delay in ms before a hover trigger opens the tooltip. Ignored by focus/manual. | |
| closeDelay | number | 100 | Delay in ms before a hover trigger closes the tooltip. Ignored by focus/manual. | |
| offset | number | 6 | Distance in pixels between trigger and tooltip. | |
| openWhen | string | undefined | 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… 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. Type Default Required | |
| anchor | string | undefined | 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… 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. Type Default Required | |
| size | "sm""md""lg" | "md" | Visual size variant. | |
| wrap | boolean | true | Whether 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). | |
| open | boolean | false | Controls the open/closed state. Bindable in Svelte — useful for the 'manual' strategy. | |
| children | Snippet<[{ 'data-tooltip-trigger': true }]> | undefined | Snippet that renders the trigger element. The tooltip wires hover/focus listeners to the node you attach the props to. |