Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Hover Card

A rich preview panel that opens on hover or focus of a trigger — interactive content, four directions, and forgiving open/close delays.

Status
stable
Since
0.1.0
Accessibility pattern
APG tooltip (hover and focus)

Last updated

Install

Add Hover Card from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/hover-card

The same command installs the registry items it composes: @vegastack/floating-surface.

Usage

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

<HoverCard>
  <HoverCardTrigger render={<Button variant="ghost">@ada</Button>} />
  <HoverCardContent>
    {/* app passes already-resolved preview content here */}
    <UserPreview user={user} />
  </HoverCardContent>
</HoverCard>;

Presentational component

HoverCard is presentational — it renders whatever children you give it and never fetches or resolves an entity. Your app owns the data: an app-side wrapper looks up the user / agent / team and passes the rendered preview (with its own loading / empty / error states) as HoverCardContent children. Keep that wrapper in the app, not in the design system.

Anatomy

HoverCard is a compound component built on Base UI's PreviewCard. Compose the parts inside the root:

HoverCard — data-slot="hover-card"
HoverCardArrow
HoverCardContent
HoverCardTrigger — data-slot="hover-card-trigger"
<HoverCard openDelay={700} closeDelay={300}>
  <HoverCardTrigger render={<Button variant="ghost">@ada</Button>} />
  <HoverCardContent side="bottom" align="center">
    {/* arbitrary, app-resolved content — an avatar + name + stats, a team summary, ... */}
    {/* <HoverCardArrow /> — or pass `arrow` to HoverCardContent */}
  </HoverCardContent>
</HoverCard>
  • HoverCard — the root; owns open/close state (open / defaultOpen / onOpenChange) and the openDelay / closeDelay. Renders no DOM of its own.
  • HoverCardTrigger — the element the card attaches to (data-slot="hover-card-trigger"). Renders an <a> by default; pass render to project the card onto a Button or any other element.
  • HoverCardContent — the floating preview panel (data-slot="hover-card-content"). Composes Base UI's Portal + Positioner + Popup, owns side / sideOffset / align / collisionPadding, exposes portalProps / positionerProps, can wrap children in an optional Base UI Viewport via viewportProps, animates enter/exit, and optionally renders an arrow.
  • HoverCardArrow — a triangle anchored to the trigger (data-slot="hover-card-arrow"). Rendered automatically when HoverCardContent gets arrow, or composed directly.

Examples

Direction

Place the card on any of four sides with side. The position flips automatically when it would collide with the viewport edge.

Alignment

Use align (start / center / end) to position the card along the chosen side. All three are shown on side="bottom" below.

Open & close delays

openDelay guards against accidental opens while the pointer passes over the trigger; closeDelay keeps the interactive card open long enough for the pointer to travel into it. Both default to a forgiving pace (700 / 300 ms). This example sets both to 0 so the card opens and closes instantly on hover.

<HoverCard openDelay={0} closeDelay={0}>
  <HoverCardTrigger render={<Button variant="ghost">@grace</Button>} />
  <HoverCardContent>{/* ... */}</HoverCardContent>
</HoverCard>

Interactive content with an arrow

Unlike a tooltip, the panel is fully interactive — the close delay gives the pointer time to travel from the trigger into the card, so links and buttons inside stay reachable.

API Reference

HoverCard

PropTypeDefaultDescription
childrenReact.ReactNodeThe trigger and content parts.
closeDelaynumber300How long to wait before closing, in milliseconds, after the pointer leaves. Gives the pointer time to travel from the trigger into the (interactive) card.
openDelaynumber700How long to wait before opening, in milliseconds, after the pointer enters the trigger. The delay guards against accidental opens while the pointer passes over.

Data attributes and CSS variables on HoverCard

AttributeValues
data-slot"hover-card"

HoverCardTrigger

HoverCardTrigger 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 HoverCardTrigger

AttributeValues
data-slot"hover-card-trigger"

HoverCardContent

PropTypeDefaultDescription
alignAlign"center"Alignment relative to the chosen side.
arrowbooleanfalseRender a directional arrow pointing at the trigger.
collisionPaddingPadding8Minimum distance to keep between the card and the viewport edge, in pixels.
portalPropsOmit<Omit<PreviewCardPortalProps, "ref"> & React.RefAttributes<HTMLDivElement>, "children">Props forwarded to the underlying Base UI PreviewCard.Portal.
positionerPropsOmit<Omit<PreviewCardPositionerProps, "ref"> & React.RefAttributes<HTMLDivElement>, "align" | "children" | "collisionPadding" | "side" | "sideOffset">Props forwarded to the underlying Base UI PreviewCard.Positioner.
sideSide"bottom"Which side of the trigger to place the card on.
sideOffsetnumber | OffsetFunction8Distance between the trigger and the card, in pixels.
viewportPropsOmit<Omit<PreviewCardViewportProps, "ref"> & React.RefAttributes<HTMLDivElement>, "children">Props forwarded to an optional Base UI PreviewCard.Viewport that wraps popup children.

HoverCardArrow

HoverCardArrow 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 card opens on pointer hover and keyboard focus of the trigger, and closes on blur or Escape — it is reachable without a mouse.
  • The openDelay guards against accidental opens; the closeDelay keeps the (interactive) card open long enough for the pointer to move into it.
  • The panel portals to the end of <body> and is positioned with collision detection so it never clips at the viewport edge.
  • Controls inside the card show a :focus-visible ring (outline-ring) — never outline: none.
KeyAction
TabMove focus to the trigger — the card opens on focus.
Tab / Shift + TabMove focus between controls inside the open card.
EscClose the card.
ContractStates tested
Behaviourdefault, empty, error, loading, open
Accessibilityfocus-visible, semantic-html
Visualdefault, active, loading, error, empty

Do / Don't

Do
Use a Hover Card for a rich, optional preview of an entity behind a trigger — a user/agent/team card with an avatar, stats, and a link. Pass already-resolved content as children and keep data-loading in an app-side wrapper.
Don't
Use a Hover Card for short plain-text labels (use a Tooltip), for content that must block the page or demand a decision (use a Dialog), or for anything essential — hover-only content isn't reliably reachable on touch.

On this page