Tooltip
Hover- and focus-activated tooltip with configurable delay and skip-delay window for sibling tooltips.
Anatomy
import { Tooltip } from "@spacing-ui/core";
<Tooltip.Provider delayDuration={700} skipDelayDuration={300}>
<Tooltip>
<Tooltip.Trigger>
<button>Hover me</button>
</Tooltip.Trigger>
<Tooltip.Content>I am a tooltip</Tooltip.Content>
</Tooltip>
</Tooltip.Provider>;Tooltip.Trigger clones its child and injects event handlers + aria-describedby — bring your own button.
API
Tooltip.Provider
| Prop | Type | Default | Description |
|---|---|---|---|
delayDuration | number | 700 | ms before opening on hover |
skipDelayDuration | number | 300 | ms after one closes during which the next opens immediately |
Tooltip (Root)
| Prop | Type | Description |
|---|---|---|
open / defaultOpen | boolean | |
onOpenChange | (open: boolean) => void | |
delayDuration | number | Per-instance override of provider delay |
Accessibility
- Content has
role="tooltip" - Trigger gets
aria-describedbypointing to content when open - Escape closes the tooltip
Examples
Icon buttons
Tooltips make icon-only buttons discoverable without adding visible labels:
<Tooltip>
<Tooltip.Trigger>
<button aria-label="Delete"><TrashIcon /></button>
</Tooltip.Trigger>
<Tooltip.Content>Delete</Tooltip.Content>
</Tooltip>The tooltip’s content is announced via aria-describedby, not aria-labelledby — keep the button’s aria-label.
Provider setup
Wrap your app once in Tooltip.Provider to share delay + skip-delay across every tooltip:
export default function RootLayout({ children }) {
return (
<Tooltip.Provider delayDuration={500} skipDelayDuration={200}>
{children}
</Tooltip.Provider>
);
}Individual tooltips can still override via the per-instance delayDuration prop.
Live demo
Testing
Tooltips open on hover and on focus, so tests can drive them via the keyboard:
const trigger = screen.getByRole("button", { name: /hover me/i });
trigger.focus();
expect(await screen.findByRole("tooltip")).toHaveTextContent(/i am a tooltip/i);Last updated on