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
- 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-cardThe 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 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 theopenDelay/closeDelay. Renders no DOM of its own.HoverCardTrigger— the element the card attaches to (data-slot="hover-card-trigger"). Renders an<a>by default; passrenderto project the card onto aButtonor any other element.HoverCardContent— the floating preview panel (data-slot="hover-card-content"). Composes Base UI's Portal + Positioner + Popup, ownsside/sideOffset/align/collisionPadding, exposesportalProps/positionerProps, can wrap children in an optional Base UI Viewport viaviewportProps, animates enter/exit, and optionally renders an arrow.HoverCardArrow— a triangle anchored to the trigger (data-slot="hover-card-arrow"). Rendered automatically whenHoverCardContentgetsarrow, 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
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | The trigger and content parts. |
closeDelay | number | 300 | How long to wait before closing, in milliseconds, after the pointer leaves. Gives the pointer time to travel from the trigger into the (interactive) card. |
openDelay | number | 700 | How 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
| Attribute | Values |
|---|---|
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
| Attribute | Values |
|---|---|
data-slot | "hover-card-trigger" |
HoverCardContent
| Prop | Type | Default | Description |
|---|---|---|---|
align | Align | "center" | Alignment relative to the chosen side. |
arrow | boolean | false | Render a directional arrow pointing at the trigger. |
collisionPadding | Padding | 8 | Minimum distance to keep between the card and the viewport edge, in pixels. |
portalProps | Omit<Omit<PreviewCardPortalProps, "ref"> & React.RefAttributes<HTMLDivElement>, "children"> | — | Props forwarded to the underlying Base UI PreviewCard.Portal. |
positionerProps | Omit<Omit<PreviewCardPositionerProps, "ref"> & React.RefAttributes<HTMLDivElement>, "align" | "children" | "collisionPadding" | "side" | "sideOffset"> | — | Props forwarded to the underlying Base UI PreviewCard.Positioner. |
side | Side | "bottom" | Which side of the trigger to place the card on. |
sideOffset | number | OffsetFunction | 8 | Distance between the trigger and the card, in pixels. |
viewportProps | Omit<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
openDelayguards against accidental opens; thecloseDelaykeeps 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-visiblering (outline-ring) — neveroutline: none.
| Key | Action |
|---|---|
| Tab | Move focus to the trigger — the card opens on focus. |
| Tab / Shift + Tab | Move focus between controls inside the open card. |
| Esc | Close the card. |
| Contract | States tested |
|---|---|
| Behaviour | default, empty, error, loading, open |
| Accessibility | focus-visible, semantic-html |
| Visual | default, active, loading, error, empty |