1
ComponentsOverlays

Tooltip

Short, non-essential hints shown from hover or focus without replacing the trigger's accessible name.

Purpose

Use Tooltip for a short, non-essential hint that appears from hover or keyboard focus. It never replaces the trigger's own label or critical visible information.

Hover help

import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";

export function Example() {
  return (
    <Tooltip>
      <TooltipTrigger render={<Button variant="outline" />}>Hover</TooltipTrigger>
      <TooltipPopup>Helpful context</TooltipPopup>
    </Tooltip>
  );
}

Installation

With the Siteplane registry alias configured, add the component and import the installed source from your app.

CLI

pnpm dlx shadcn@4.16.1 add @siteplane/tooltip

Import

import { Tooltip } from "@/components/ui/tooltip";
import { TooltipCreateHandle, TooltipPopup, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { TooltipContent } from "@/components/ui/tooltip";

Need the machine-readable source metadata? View registry JSON.

Usage

Use Tooltip to explain non-essential UI affordances on hover or focus. Do not use it for required instructions.

import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";

export function Example() {
  return (
    <Tooltip>
      <TooltipTrigger render={<Button variant="outline" />}>Hover</TooltipTrigger>
      <TooltipPopup>Helpful context</TooltipPopup>
    </Tooltip>
  );
}

Examples

Placement, disabled triggers, provider grouping, shared handles and icon-button hints are props and compositions of the same primitive.

Placement

The side prop positions the popup around the trigger. The built-in arrow follows automatically.

{/* side: "top" | "bottom" | "left" | "right", default "top" */}
<Tooltip>
  <TooltipTrigger render={<Button variant="outline" />}>Bottom</TooltipTrigger>
  <TooltipPopup side="bottom">Tooltip on bottom</TooltipPopup>
</Tooltip>

Disabled trigger

Disabled root and trigger keep the popup closed for disabled controls.

<Tooltip disabled>
  <TooltipTrigger disabled render={<Button disabled variant="outline" />}>
    Unavailable
  </TooltipTrigger>
  <TooltipPopup>Disabled controls do not show this hint.</TooltipPopup>
</Tooltip>

Grouped tooltips

TooltipProvider shares timing across a toolbar, so moving between triggers feels instant.

<TooltipProvider>
  <ToggleGroup defaultValue={["bold"]} multiple variant="outline">
    <Tooltip>
      <TooltipTrigger
        render={<ToggleGroupItem aria-label="Toggle bold" value="bold" />}
      >
        <BoldIcon aria-hidden="true" />
      </TooltipTrigger>
      <TooltipPopup>Bold</TooltipPopup>
    </Tooltip>
    <Tooltip>
      <TooltipTrigger
        render={<ToggleGroupItem aria-label="Toggle italic" value="italic" />}
      >
        <ItalicIcon aria-hidden="true" />
      </TooltipTrigger>
      <TooltipPopup>Italic</TooltipPopup>
    </Tooltip>
  </ToggleGroup>
</TooltipProvider>

Shared tooltip

TooltipCreateHandle connects detached triggers to one shared popup that moves between them.

const tooltipHandle = TooltipCreateHandle<ComponentType>();

<TooltipProvider>
  <ToggleGroup defaultValue={["bold"]} multiple variant="outline">
    <TooltipTrigger
      handle={tooltipHandle}
      payload={BoldContent}
      render={<ToggleGroupItem aria-label="Toggle bold" value="bold" />}
    >
      <BoldIcon aria-hidden="true" />
    </TooltipTrigger>
    <TooltipTrigger
      handle={tooltipHandle}
      payload={ItalicContent}
      render={<ToggleGroupItem aria-label="Toggle italic" value="italic" />}
    >
      <ItalicIcon aria-hidden="true" />
    </TooltipTrigger>
  </ToggleGroup>
  <Tooltip handle={tooltipHandle}>
    {({ payload: Payload }) => (
      <TooltipPopup>{Payload !== undefined && <Payload />}</TooltipPopup>
    )}
  </Tooltip>
</TooltipProvider>

Icon button hint

A tooltip names an icon-only action. Critical status and instructions stay visible outside the tooltip.

Invoice export

Critical status, errors and instructions stay visible outside the tooltip.

<div className="flex items-center justify-between gap-3">
  <span className="font-medium">Invoice export</span>
  <Tooltip>
    <TooltipTrigger
      render={
        <Button aria-label="Delete export preset" size="icon-sm" variant="ghost" />
      }
    >
      <TrashIcon aria-hidden="true" />
    </TooltipTrigger>
    <TooltipPopup>Delete preset</TooltipPopup>
  </Tooltip>
</div>

API Reference

The reference lists the source-owned exports and props that are easy to miss in visual examples. Inherited Base UI props remain available unless the wrapper narrows them.

  • Mount TooltipProvider around a dense tooltip group, then compose Tooltip, TooltipTrigger and TooltipPopup.
  • TooltipPopup accepts side, align="start" | "center" | "end", sideOffset, alignOffset, anchor and popup props.
  • anchor can position the popup from another element or virtual position instead of the normal trigger.
  • TooltipCreateHandle supports detached trigger relationships; TooltipContent remains a compatibility alias for TooltipPopup.

Motion

Component motion uses the shared Siteplane motion contract. See the global motion guide for provider setup, tokens and reduced-motion behavior.

  • Tooltip uses the shared floating-panel fade, scale and directional offset tokens.
  • Grouped tooltips can skip the repeated opening delay after the first tooltip; reduced motion remains global.

Accessibility

  • The trigger needs its own visible name or aria-label; Tooltip content is supporting description only.
  • Do not place interactive controls inside TooltipPopup.
  • Use visible helper text, Alert or Toast for critical and screen-reader-relevant messages.

Implementation Guidance

  • Use TooltipProvider for dense groups such as toolbars, not as a replacement for visible labels.
  • Prefer the normal trigger relationship and use anchor or TooltipCreateHandle only for a genuine detached case.
  • Keep portal, positioning and motion internals inside the primitive.

On This Page