SuperAPI AI Directory

shadcn-syntax-popover-tooltip-hovercard

Frontend & websites

View repository

Updated 10-15 22:18

About this skill

shadcn ui : Popover vs Tooltip vs HoverCard

Three Radix-backed floating-panel primitives ship in shadcn ui new-york-v4. They look similar (a panel anchored to a trigger) but solve three disjoint UX problems. Picking the wrong one breaks keyboard navigation, touch users, screen readers, or all three at once. This skill is a decision tree, not three API surfaces stacked together. The full prop tables live in references/methods.md; the working recipes live in references/examples.md; the failure modes live in references/anti-patterns.md.

The single decision

Is the panel content interactive (inputs, links, buttons, anything
keyboard-focusable)?
├─ NO  -> Is the hint a short, plain-text label (one sentence max)?
│         ├─ YES -> Tooltip
│         └─ NO  -> Popover (open it on click, even for read-only content)
└─ YES -> Should the panel appear on hover, or on click?
          ├─ HOVER (and the trigger is keyboard-reachable too) -> HoverCard
          └─ CLICK                                              -> Popover

Three additional gates ALWAYS override the table above :

  1. If the trigger is on a touch device and the content is critical : NEVER Tooltip. Tooltip is hidden on touch by Radix design. Use Popover instead.
  2. If the content contains a <form> with a Submit button : NEVER Tooltip and NEVER HoverCard. Use Popover; both Tooltip and HoverCard close on pointer-leave, which races the form submission.
  3. If the content is keyboard-only-reachable via the trigger : NEVER Tooltip and NEVER HoverCard. Tooltip cannot host interactive elements; HoverCard is hover-only and fails WCAG 2.1.1 Keyboard. Use Popover.

Decision Matrix at a glance

Need Primitive Trigger Content a11y
One-line label on icon button Tooltip hover / focus text only aria-describedby
One-line label on disabled button Tooltip (wrap a <span>) hover / focus text only aria-describedby
Rich profile preview on mention HoverCard hover / focus text + image + links aria-haspopup
Date picker calendar Popover click Calendar component focus moves into panel
Colour picker Popover click Sliders / inputs focus moves into panel
Emoji picker Popover click grid + search input focus moves into panel
Help text balloon below a form Label Tooltip hover / focus text only aria-describedby
Confirm action panel ("are you sure?") Popover click text + 2 buttons focus moves into panel
Filter popover for a table column Popover click inputs + chips focus moves into panel

The three primitives : compact signatures

ALL three components require "use client". ALL three ship as kebab-case CLI installs and import from @/components/ui/{popover,tooltip,hover-card}.

Popover

"use client"

import {
  Popover,
  PopoverTrigger,
  PopoverContent,
} from "@/components/ui/popover"
import { Button } from "@/components/ui/button"

<Popover>
  <PopoverTrigger asChild>
    <Button variant="outline">Open</Button>
  </PopoverTrigger>
  <PopoverContent className="w-80">
    Anything interactive lives here.
  </PopoverContent>
</Popover>

Install : pnpm dlx shadcn@latest add popover.

PopoverContent shadcn defaults : align="center", sideOffset={4}. Wraps Radix PopoverPrimitive.Content inside PopoverPrimitive.Portal automatically. modal defaults to false; set modal={true} ONLY when the popover must trap focus (rare; Dialog is usually a better fit).

Also exported : PopoverAnchor (decouple positioning anchor from the trigger), PopoverHeader, PopoverTitle, PopoverDescription (semantic composition helpers, optional).

Tooltip

"use client"

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

// At app root (layout / _app / main.tsx) :
<TooltipProvider>{children}</TooltipProvider>

// At usage site :
<Tooltip>
  <TooltipTrigger asChild>
    <button aria-label="Save">
      <SaveIcon />
    </button>
  </TooltipTrigger>
  <TooltipContent>Save (Ctrl+S)</TooltipContent>
</Tooltip>

Install : pnpm dlx shadcn@latest add tooltip.

ALWAYS mount <TooltipProvider> at the app root. Every <Tooltip> is a silent no-op without an enclosing Provider. The shadcn TooltipProvider ships delayDuration={0} (immediate tooltips); the underlying Radix default is 700. Override per-Tooltip with <Tooltip delayDuration={500}> when a slower hover-in is appropriate. TooltipContent shadcn default : sideOffset={0} (Radix default is 0 too) plus a built-in <TooltipPrimitive.Arrow/> rendered inside the content. Wraps Radix TooltipPrimitive.Content inside a Portal.

Tooltip content MUST be plain text. NEVER place buttons, links, inputs, or any focusable element inside <TooltipContent>. The Tooltip is hidden on touch devices by Radix design; treat the hint as decorative on mobile.

HoverCard

"use client"

import {
  HoverCard,
  HoverCardTrigger,
  HoverCardContent,
} from "@/components/ui/hover-card"

<HoverCard openDelay={400} closeDelay={200}>
  <HoverCardTrigger asChild>
    <a href="/users/freek">@freek</a>
  </HoverCardTrigger>
  <HoverCardContent className="w-80">
    Rich profile preview, including avatar, bio, follow button.
  </HoverCardContent>
</HoverCard>

Install : pnpm dlx shadcn@latest add hover-card.

NO provider needed; HoverCard is self-contained. Radix defaults : openDelay={700}, closeDelay={300}. Override per-instance for hover-flicker-prone UIs (avatar lists, mention lists, link-preview hovers) : openDelay={400} is a good baseline. HoverCardContent shadcn defaults : align="center", sideOffset={4}. Wraps Radix HoverCardPrimitive.Content inside a Portal.

HoverCard works on touch (tap to open, tap-outside to close), but the trigger MUST be a real focusable element (anchor, button, or tabIndex={0} element) so keyboard users can reach the content via focus + Enter.

TooltipProvider placement rule

The <TooltipProvider> is the ONE mandatory wrapper of the three primitives. Place it ONCE at the app root :

  • Next.js App Router : in app/layout.tsx, wrap {children} inside the <body>.
  • Next.js Pages Router : in pages/_app.tsx, wrap <Component {...pageProps} />.
  • Vite / SPA : in src/main.tsx (or wherever you mount <App/>), wrap <App/>.
  • Remix : in app/root.tsx, wrap the <Outlet/>.

ALWAYS one Provider per render tree. Nested Providers compound delayDuration resets and break the skipDelayDuration skip-window; nesting is supported by Radix but ALWAYS unnecessary and confusing.

Per-instance overrides go on the <Tooltip> itself, not on a new Provider :

<Tooltip delayDuration={500} disableHoverableContent>
  <TooltipTrigger asChild>
    <button>?</button>
  </TooltipTrigger>
  <TooltipContent>Slow hint.</TooltipContent>
</Tooltip>

HoverCard and Popover do NOT need a provider. There is no HoverCardProvider or PopoverProvider in the shadcn ui surface.

Touch-device behaviour matrix

Primitive Touch behaviour Implication
Tooltip Hidden by default on touch (Radix design choice) NEVER use Tooltip for critical info
HoverCard Opens on tap; closes on tap-outside Works on mobile but watch z-index
Popover Opens on click (same as tap); no hover dependence Always works on touch

Verify this in the official shadcn docs (/docs/components/tooltip) : "Tooltips remain hidden on touch devices unless explicitly configured otherwise." The shadcn position is to honour the Radix default; do NOT patch the underlying Tooltip.Provider to force-show on touch. Instead, escalate to Popover when the hint is essential.

Accessibility matrix

Primitive ARIA on content Focus behaviour on open Keyboard reachable?
Popover none required; content is generic focus moves into Content (first focusable element) YES
Tooltip aria-describedby links trigger to content NO focus move (Tooltip never receives focus) NO (content text is read via aria-describedby)
HoverCard aria-haspopup on the trigger NO focus move (hover/focus parity, content is content-only) YES (focus on trigger opens it, Tab moves into content)

Tooltip MUST receive its label-content via plain text children; screen readers read it via aria-describedby. NEVER nest a Button, Link, or focusable element inside <TooltipContent> : the screen reader cannot reach those elements through aria-describedby and keyboard users have no way to interact with them.

HoverCard's trigger MUST be a real focusable element (button, anchor, or tabIndex={0}). The keyboard accessibility for HoverCard is "focus the trigger -> content opens -> Tab moves into content"; if the trigger is a div without tabIndex, the HoverCard is keyboard-inaccessible (WCAG 2.1.1 violation).

Popover does its own focus management : focus moves to the first focusable element inside PopoverContent on open, and restores to the trigger on close. With modal={true}, Popover ALSO traps focus inside the content until close; with modal={false} (the default), focus can be tabbed out into the page (a desktop hover-menu UX, rarely what you want).

Common props that ALL three Content components share

All three *Content components render Radix Popper-positioned floating elements and accept the same positioning prop surface : side ("top"|"right"|"bottom"|"left", default "bottom"), sideOffset (defaults : Popover 4, HoverCard 4, Tooltip 0), align ("start"|"center"|"end", default "center" on Popover and HoverCard, "center" on Tooltip too), alignOffset (default 0), avoidCollisions (default true), collisionBoundary, collisionPadding, sticky ("partial"|"always", default "partial"), hideWhenDetached (default false), forceMount. The Tooltip Content additionally renders an <Arrow/> element internally. The full per-component prop matrix lives in references/methods.md.

Companion Skills

Read these from skills/source/shadcn-syntax/ on demand only :

  • shadcn-syntax-dialog : if the panel must trap focus AND host a multi-step form, use Dialog instead of Popover modal={true}. Dialog is the modal primitive; Popover modal is a niche escape hatch.
  • shadcn-syntax-sheet : side-anchored modal panel; same rule as Dialog but for slide-in side panels.
  • shadcn-syntax-menu-primitives : DropdownMenu / ContextMenu / Menubar / NavigationMenu. ALWAYS prefer a DropdownMenu over a Popover-containing-buttons; menus have built-in roving-tabindex keyboard navigation that Popovers lack.
  • shadcn-syntax-toast-sonner : for ephemeral notifications use Sonner toasts, NEVER a Tooltip or HoverCard (toasts are page-level, the three primitives in this skill are anchored to a trigger).
  • shadcn-syntax-calendar-datepicker : the canonical Popover + Calendar composition for date pickers.
  • shadcn-errors-radix-controlled : controlled-state pitfalls (open without onOpenChange, Portal z-index surprises, asChild misuse). Read when debugging a panel that "is stuck open" or "won't close".

Reference Links

Detailed prop tables : references/methods.md. Working recipes : references/examples.md. Failure modes and fixes : references/anti-patterns.md.