Tooltip
A visual popup that explains an element on hover or keyboard focus — four sides, a shared delay, and a collision-aware arrow.
- Status
- Since
0.1.0- Accessibility pattern
- visual-only popup; the trigger carries the name
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Tooltip before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify tooltipInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/tooltipRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
Usage
Add the TooltipProvider once, at the root of your app — it owns the delay every tooltip shares.
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
<Tooltip>
<TooltipTrigger>Hover</TooltipTrigger>
<TooltipContent>
<p>Add to library</p>
</TooltipContent>
</Tooltip>;Anatomy
Tooltip is a compound component. Compose the parts inside the root:
TooltipProvider
└── Tooltip
├── TooltipTrigger
└── TooltipContentTooltipProvider— the app-root wrapper that shares one delay across every tooltip, so moving between neighbouring triggers does not re-pay it. The app-rootVegaStackProvidersets it toTIMINGS.tooltipOpenDelayMs(250ms).Tooltip— the root that owns open state (open,defaultOpen,onOpenChange) and can be disabled outright (data-slot="tooltip").TooltipTrigger— the element the popup describes (data-slot="tooltip-trigger"). Userenderto make an existing control the trigger rather than nesting a second button inside it.TooltipContent— the portaled popup (data-slot="tooltip-content") plus its positioner and arrow. It takes the positioner'sside,sideOffset,alignandalignOffsetdirectly.
Examples
Composition
TooltipTrigger renders the element you give it, and TooltipContent renders in a portal — so the
popup escapes any overflow: hidden ancestor. The trigger keeps its own accessible name; see
Accessibility for why that is not optional.
Side
Use the side prop to change the position of the tooltip. It accepts the physical sides top,
right, bottom and left as well as the logical inline-start and inline-end, and flips away
from a viewport edge on its own.
With Keyboard Shortcut
Put a Kbd inside the content to show the shortcut the control answers to. The popup reserves the
trailing padding for it automatically.
Disabled Button
A disabled native button fires no pointer events, so wrap it in a span and make the span the
trigger. This is the case the Button component's own disabled handling (FRM-4) exists to avoid —
reach for the wrapper only when the control is genuinely a native disabled element.
RTL
The popup, its arrow and the logical sides mirror in a right-to-left document. Pass dir on the
content when the tooltip's text runs the other way from the page.
API Reference
Tooltip
Tooltip adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
TooltipProvider
TooltipProvider adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
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).
TooltipContent
TooltipContent adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Accessibility
- The popup is visual-only. Base UI renders it with no
role="tooltip"and wires noaria-describedbyon the trigger — that is its documented design, not an oversight. The tooltip's text therefore reaches no screen reader on its own. - So the trigger must carry the name itself. Give every trigger an
aria-labelthat closely matches the popup's text, exactly as the fixtures on this page do. Without it, an icon-only trigger is unnamed for assistive technology even though the tooltip looks informative. - It opens on hover and on keyboard focus, and Escape closes it — a tooltip that only answers the mouse is unreachable by keyboard.
- Nothing interactive belongs inside the popup: it disappears on blur, so a link or button in there can never be reached.
- The popup is portaled out of the trigger's DOM subtree;
TooltipContentre-applies the nested theme scope on the positioner (OVL-13), so a tooltip raised inside a scoped surface keeps that surface's palette. - Motion is limited to a fade and a small scale, and the global reduced-motion reset in
base.csscollapses both.
| Key | Action |
|---|---|
| Tab | Focus the trigger, which opens the tooltip. |
| Escape | Close the open tooltip. |
| Contract | States tested |
|---|---|
| Behaviour | default, open |
| Accessibility | semantic-html |
| Visual | default, hover |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/tooltip.patch. Every hunk:
- OVL-13 —
TooltipContentreadsuseInternalThemeScope()and attaches it to the Positioner inside the Portal. A portaled popup leaves its theme subtree, so a nested scope is lost by inheritance unless the host re-applies it;tooling/verify-portal-theme-scope.mjsinventories this host. - OVL-14 —
TooltipContentforwards an optionalcontainerto its Portal. Upstream forwards none, and a portal defaults todocument.body, which the Fullscreen API paints the fullscreen element over; a fullscreen player's control labels rendered invisibly behind it. Leavingcontainerunset is upstream's own default, so nothing else moves. - DOC-2 —
cnis imported from@vegastack/design.