Tooltip
A floating label that appears on hover or focus — smart shared delay, rich content, optional keyboard hints, and collision-aware positioning.
- Status
- Since
0.1.0- Accessibility pattern
- APG tooltip
Last updated
Install
Add Tooltip from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/tooltipThe same command installs the registry items it composes: @vegastack/kbd, @vegastack/floating-surface.
Usage
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
<Tooltip>
<TooltipTrigger render={<Button variant="outline">Hover me</Button>} />
<TooltipContent>Add to your library</TooltipContent>
</Tooltip>;Provider
Tooltip needs a single Tooltip.Provider ancestor that owns the shared
open/close delay. In VegaStack apps it is already mounted inside
VegaStackProvider, so you never render it yourself — just drop tooltips
anywhere below the app root. Once one tooltip opens, neighbouring tooltips open
instantly (skip delay).
For a standalone tree (Storybook, tests, an isolated widget) wrap the region in
the re-exported TooltipProvider.
Anatomy
Tooltip is a compound component. Compose the flat parts inside the root:
<Tooltip delay={400}>
<TooltipTrigger render={<Button />} />
<TooltipContent side="top" arrow>
Label
<TooltipKbd keys={["⌘", "K"]} />
</TooltipContent>
</Tooltip>Tooltip— the root (data-slot="tooltip"). Groups the trigger and content; renders no DOM of its own. Acceptsdelayto override the provider's shared delay.TooltipTrigger— the anchored element (data-slot="tooltip-trigger"). Renders a<button>; passrenderto project the tooltip onto your own element (Base UIrendercomposition).TooltipContent— the floating panel (data-slot="tooltip-content"). Bundles Base UI'sPortal+Positioner+Popupinto one part, exposesportalProps/positionerProps, and can wrap children in an optional Base UI Viewport viaviewportProps. Themed with the invertedbg-foreground/text-backgroundsurface; animated viadata-[starting-style]/data-[ending-style].TooltipArrow— an optional triangle pointing at the trigger (data-slot="tooltip-arrow"). Rendered automatically whenTooltipContentgetsarrow, or compose it directly.TooltipKbd— render a keyboard shortcut hint as styled<kbd>keys (data-slot="tooltip-kbd").
Examples
Positioning
Set side (top / right / bottom / left) and sideOffset on the
content. sideOffset accepts Base UI Positioner offset values, including offset functions. The
tooltip flips automatically to avoid viewport collisions.
Alignment
Use align (start / center / end) to shift the tooltip along the chosen
side. The default is center.
Offset
sideOffset controls the gap between the trigger and the tooltip (default
8, the shared detached-panel offset). Pass a number for a fixed gap, or an
offset function ({ side, align, anchor, positioner }) => number to resolve
the distance per placement.
<TooltipContent side="top" sideOffset={({ side }) => (side === "top" ? 24 : 8)}>
Resolved per side
</TooltipContent>Delay
Set delay (in milliseconds) on the root to override the provider's shared
open delay for a single tooltip — delay={0} opens instantly, a larger value
makes it more deliberate.
Keyboard hint
Pair a label with a shortcut using TooltipKbd. Pass an array of key tokens
(or a string, split per glyph).
Arrow
Set arrow on the content to render a directional pointer.
Playground
Pick a side and toggle the arrow, hover the trigger, then copy the generated JSX.
<Tooltip>
<TooltipTrigger render={<Button variant="outline">Hover me</Button>} />
<TooltipContent>Add to your library</TooltipContent>
</Tooltip>API Reference
Tooltip
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | The trigger and content parts. |
delay | number | — | How long to wait before opening, in milliseconds. Forwarded to the trigger. Falls back to the provider's shared delay when omitted. |
Data attributes and CSS variables on Tooltip
| Attribute | Values |
|---|---|
data-slot | "tooltip" |
TooltipTrigger
TooltipTrigger adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Data attributes and CSS variables on TooltipTrigger
| Attribute | Values |
|---|---|
data-slot | "tooltip-trigger" |
TooltipContent
| Prop | Type | Default | Description |
|---|---|---|---|
align | Align | 'center' | Alignment relative to the chosen side. |
arrow | boolean | false | Render a directional arrow pointing at the trigger. |
portalProps | Omit<Omit<TooltipPortalProps, "ref"> & React.RefAttributes<HTMLDivElement>, "children"> | — | Props forwarded to the underlying Base UI Portal. |
positionerProps | Omit<Omit<TooltipPositionerProps, "ref"> & React.RefAttributes<HTMLDivElement>, "align" | "children" | "side" | "sideOffset"> | — | Props forwarded to the underlying Base UI Positioner. |
side | Side | 'top' | Which side of the trigger to place the tooltip on. |
sideOffset | number | OffsetFunction | 8 | Distance between the trigger and the tooltip, in pixels. |
viewportProps | Omit<Omit<TooltipViewportProps, "ref"> & React.RefAttributes<HTMLDivElement>, "children"> | — | Props forwarded to an optional Base UI Viewport that wraps tooltip children. |
TooltipArrow
TooltipArrow adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
TooltipKbd
| Prop | Type | Default | Description |
|---|---|---|---|
keys* | string | readonly string[] | — | The shortcut — a string (split per glyph) or explicit key tokens. |
os | "mac" | "other" | 'other' | Platform label mode, forwarded to Kbd. Resolve it at the call site with
usePlatform(); the default matches that hook's SSR fallback. |
Data attributes and CSS variables on TooltipKbd
| Attribute | Values |
|---|---|
data-slot | "tooltip-kbd" |
Accessibility
- The popup carries
role="tooltip", so assistive technology announces it as a tooltip and associates it with the trigger. - Tooltips open on hover and keyboard focus — never hover-only — so keyboard and screen-reader users get the same hint.
- Hover/focus-only by design — no touch trigger. This is a Base UI upstream constraint, not a bug:
Tooltipopens onpointerenter/focus and has no touch-tap handling, so on phones/tablets (any device that can't hover) content that lives only inside a tooltip is permanently unreachable. Never make tooltip content essential-only — the trigger's own label/icon must stand on its own. When the tooltip exists specifically to reveal overflow/clipped text, reach forTruncatedTextinstead of a bareTooltip— it detects(hover: none)and swaps the hover-only tooltip for a tap-to-toggle disclosure on touch, while keeping the Tooltip (and its focus trigger) unchanged for keyboard/mouse users. That component is the reference pattern for touch disclosure of clipped content. - A tooltip is a supplement, not a replacement for an accessible name. Give icon-only triggers their own
aria-label; don't rely on the tooltip alone. - Content is non-interactive by design (it dismisses on blur/escape). Put actions in a Popover, not a Tooltip.
| Key | Action |
|---|---|
| Tab | Move focus to the trigger — the tooltip opens. |
| Esc | Dismiss the open tooltip. |
| Contract | States tested |
|---|---|
| Behaviour | default, open |
| Accessibility | semantic-html |
| Visual | default, hover |