Skip to content
Component installs need the registry setup
VegaStack Design

Tooltip

A visual popup that explains an element on hover or keyboard focus — four sides, a shared delay, and a collision-aware arrow.

Status
stable
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 tooltip

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/tooltip

Run 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:

Tooltip — data-slot="tooltip"
TooltipContent — data-slot="tooltip-content"
TooltipProvider — data-slot="tooltip-provider"
TooltipTrigger — data-slot="tooltip-trigger"
TooltipProvider
└── Tooltip
    ├── TooltipTrigger
    └── TooltipContent
  • TooltipProvider — the app-root wrapper that shares one delay across every tooltip, so moving between neighbouring triggers does not re-pay it. The app-root VegaStackProvider sets it to TIMINGS.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"). Use render to 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's side, sideOffset, align and alignOffset directly.

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 no aria-describedby on 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-label that 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; TooltipContent re-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.css collapses both.
KeyAction
TabFocus the trigger, which opens the tooltip.
EscapeClose the open tooltip.
ContractStates tested
Behaviourdefault, open
Accessibilitysemantic-html
Visualdefault, hover

Do / Don't

Do
Give the trigger an aria-label matching the tooltip's text — the popup itself is visual-only and announces nothing.
Don't
Put a link, a button or any content the user must reach inside a tooltip — it disappears on blur.

Deviations

Upstream's file plus packages/ui/upstream/patches/tooltip.patch. Every hunk:

  • OVL-13 — TooltipContent reads useInternalThemeScope() 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.mjs inventories this host.
  • OVL-14 — TooltipContent forwards an optional container to its Portal. Upstream forwards none, and a portal defaults to document.body, which the Fullscreen API paints the fullscreen element over; a fullscreen player's control labels rendered invisibly behind it. Leaving container unset is upstream's own default, so nothing else moves.
  • DOC-2 — cn is imported from @vegastack/design.

On this page