Tooltip
A short label for a control, shown on hover and keyboard focus.
Basic
Hover the button, or Tab to it. The tooltip describes the button and never takes focus.
Code
<Tooltip content="Add to library">
<Button slot="trigger">Hover</Button>
</Tooltip>Usage
import { Tooltip, TooltipProvider, TooltipTrigger } from "@elements/ui/tooltip";
<Tooltip content="Add to library">
<Button slot="trigger" icon label="Add"><Icon name="plus" /></Button>
</Tooltip>
<!-- one delay for a region; the next tooltip opens at once -->
<TooltipProvider delay={300}>...</TooltipProvider>Examples
Side
side picks where it opens; it flips when there is no room.
Code
<Tooltip side="left" content="Add to library">
<Button slot="trigger">Left</Button>
</Tooltip>RTL
inline-start and inline-end follow the text direction: in a right-to-left region inline-start shows on the right and inline-end on the left.
Code
<div dir="rtl">
<Tooltip side="inline-start" content="أضف إلى المكتبة">
<Button slot="trigger">بداية السطر</Button>
</Tooltip>
<Tooltip side="inline-end" content="أضف إلى المكتبة">
<Button slot="trigger">نهاية السطر</Button>
</Tooltip>
</div>With a keyboard shortcut
Rich content goes in the default slot; a kbd reads on the tooltip's fill.
Code
<Tooltip>
<Button slot="trigger" icon label="Save"><Icon name="download" /></Button>
Save changes <kbd>S</kbd>
</Tooltip>On a text field
A tooltip on a field in a form opens on keyboard focus and stays while the user types.
Code
<form>
<Tooltip content="Letters, digits and spaces">
<input slot="trigger" aria-label="Display name">
</Tooltip>
</form>Disabled button
A disabled button takes no pointer events or focus, so wrap it in a focusable span and put the tooltip on the span.
Code
<!-- a disabled button takes no pointer or focus: wrap it -->
<Tooltip content="You need edit access">
<span slot="trigger" tabindex="0">
<Button disabled>Disabled</Button>
</span>
</Tooltip>Delay group
TooltipProvider sets the delays for a region. Once one tooltip shows, the next opens at once as the pointer moves along.
Code
<TooltipProvider delay={500} skipDelay={300}>
<Tooltip content="Bold"><Button slot="trigger" ...>...</Button></Tooltip>
<Tooltip content="Italic">...</Tooltip>
<Tooltip content="Underline">...</Tooltip>
</TooltipProvider>No delay
delay={0} shows it the moment the pointer arrives.
Code
<Tooltip delay={0} content="No wait">
<Button slot="trigger">Instant</Button>
</Tooltip>Controlled and disabled
Bind open to state; onOpenChange reports the tooltip's own changes. disabled keeps it from opening.
Code
<Tooltip open={state.tip} content="Controlled"
onOpenChange={(open) => log(open)}>
<Button slot="trigger">Target</Button>
</Tooltip>
<Tooltip disabled={state.off} content="...">...</Tooltip>Follow the pointer
trackCursorAxis follows the pointer along x, y or both while it moves over the trigger.
Code
<Tooltip trackCursorAxis="x" delay={0} content="Follows the pointer">
<div slot="trigger" class="track">Move along me</div>
</Tooltip>
<Tooltip trackCursorAxis="both" delay={0} content="Follows on both axes">
<div slot="trigger" class="track-area">Move anywhere in me</div>
</Tooltip>Not hoverable
By default the pointer can move from the button onto the tooltip. disableHoverableContent closes it as the pointer leaves the button.
Code
<Tooltip disableHoverableContent content="Closes as the pointer leaves the button">
<Button slot="trigger">Not hoverable</Button>
</Tooltip>Label and closing
label gives screen readers a different text than the visible one. closeOnClick={false} keeps it open when the trigger is pressed. closeDelay keeps it a moment after the pointer leaves; TooltipProvider sets it for a region.
Code
<Tooltip content="Delete" label="Delete the draft">
<Button slot="trigger" icon label="Delete"><Icon name="trash" /></Button>
</Tooltip>
<Tooltip closeOnClick={false} content="Stays open on click">...</Tooltip>
<Tooltip closeDelay={500} content="Lingers for 500ms">...</Tooltip>
<TooltipProvider closeDelay={400}>...</TooltipProvider>Align, offsets and anchor
align lines the tooltip up with the trigger's start, center or end; sideOffset and alignOffset move it; anchor positions it against another element.
Code
<Tooltip side="bottom" align="start" content="Lined up with the start">...</Tooltip>
<Tooltip side="bottom" align="start" sideOffset={12} alignOffset={8} content="Offset 12, 8">...</Tooltip>
<Tooltip anchor="#unsaved" content="Two files changed">
<Button slot="trigger">Changes</Button>
</Tooltip>
<span id="unsaved">Unsaved</span>Function offsets
sideOffset and alignOffset also take a function of the anchor's and the tooltip's sizes, the side and the alignment.
Code
<Tooltip side="bottom" sideOffset={(d) => d.anchor.height} content="One trigger height below">
<Button slot="trigger">Offset</Button>
</Tooltip>Collision boundary, sticky and arrow padding
collisionBoundary keeps the tooltip inside an element as well as the viewport. sticky="always" keeps it in the boundary after its trigger leaves. arrowPadding keeps the arrow off the corners.
Scroll this box while one is open.
Code
<div id="panel">
<Tooltip side="bottom" align="start" collisionBoundary="#panel" content="...">...</Tooltip>
</div>
<Tooltip side="right" sticky="always" collisionBoundary="#list" content="...">...</Tooltip>
<Tooltip side="bottom" align="start" arrowPadding={40} content="...">...</Tooltip>Change reasons
onOpenChange gets a second argument: the reason for the change, the event, and cancel() to keep the current state.
Code
<!-- details.trigger is the trigger element that caused the change, if one did -->
<Tooltip content="Save" onOpenChange={(open, details) => log(open, details.reason, details.trigger)}>
<Button slot="trigger">Save</Button>
</Tooltip>
<!-- details.cancel() keeps the current state -->
<Tooltip content="..." onOpenChange={(open, details) => details.reason === "escape-key" && details.cancel()}>...</Tooltip>Hide when detached
hideWhenDetached hides the tooltip while its trigger is scrolled out of view.
Tab to the button, then scroll this box.
Code
<div class="scroller">
<Tooltip hideWhenDetached content="Hidden while out of view">
<Button slot="trigger">Focus me, then scroll</Button>
</Tooltip>
</div>Events
onOpenChangeComplete fires after each animation; onEscapeKeyDown and onPointerDownOutside fire before it closes, and preventDefault keeps it open.
Code
<Tooltip content="Counted" onOpenChangeComplete={(open) => log(open)}
onEscapeKeyDown={(e) => count(e)}
onPointerDownOutside={(e) => count(e)}>...</Tooltip>
<Button onclick={() => state.pinned = !state.pinned}>Pin tooltip</Button>
<Tooltip open={state.pinned} content="Pinned"
onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}>...</Tooltip>Collision avoidance and position method
collisionAvoidance tunes how it stays in view: side "none" keeps the side with no flip while align "shift" still slides it into view; side "shift" keeps the side and slides it over the trigger into view; fallbackAxisSide "start" tries the inline start side before the end. positionMethod="absolute" places it in page coordinates.
Code
<Tooltip side="top" collisionAvoidance={{ side: "none", align: "shift" }} content="...">...</Tooltip>
<Tooltip side="top" collisionAvoidance={{ side: "shift" }} content="...">...</Tooltip>
<Tooltip side="top" collisionAvoidance={{ fallbackAxisSide: "start" }}>...</Tooltip>
<Tooltip side="bottom" positionMethod="absolute" content="...">...</Tooltip>Collisions
collisionPadding sets the room kept from the viewport edge; avoidCollisions={false} keeps it on its side with no flip or shift. TooltipProvider disableHoverableContent applies to every tooltip inside.
Code
<Tooltip side="bottom" align="end" collisionPadding={24} content="...">...</Tooltip>
<Tooltip side="left" avoidCollisions={false} content="...">...</Tooltip>
<TooltipProvider disableHoverableContent>...</TooltipProvider>Multiple triggers
TooltipTrigger with a handle shows one tooltip for several triggers. It sits against the trigger under the pointer and moves along with it; payload carries that trigger's data, and only that trigger is described by it. Tab between them and PopoverViewport cross-fades the label.
Code
<TooltipTrigger handle="format" payload="Bold"><Button icon label="Bold">B</Button></TooltipTrigger>
<TooltipTrigger handle="format" payload="Italic"><Button icon label="Italic">I</Button></TooltipTrigger>
<!-- open, triggerId and payload are two-way: set triggerId and open to show it on a chosen trigger -->
<Tooltip handle="format" open={state.open} triggerId={state.trigger} payload={state.tool}>
<PopoverViewport>{state.tool}</PopoverViewport>
</Tooltip>Keep mounted
keepMounted keeps the closed tooltip in the page, hidden, rather than removing it.
Code
<Tooltip content="Kept in the page" keepMounted>
<Button slot="trigger">Kept</Button>
</Tooltip>Disable anchor tracking
disableAnchorTracking places the tooltip once when it opens; it stays where it is while the page scrolls.
Code
<Tooltip content="Placed once" disableAnchorTracking>
<Button slot="trigger">Pinned in place</Button>
</Tooltip>Open on load
open={true} without a binding: the tooltip shows when the page loads, then owns its state.
Code
<Tooltip open={true} content="Open on load">
<Button slot="trigger">Tip</Button>
</Tooltip>