Hover Card React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React hover card: a rich preview of a person, listing or link on hover or focus, opening as a bottom sheet on touch screens.
Rest the pointer on a link or a name and after half a second the preview grows out of it on a quick spring. Move onto the card and it stays open, so its buttons and links work; leave both and it fades out 200ms later. Tab to the trigger and it opens the same way; Escape or moving focus on closes it. The card is a supplement: its content is skipped by Tab and every action in it is reachable elsewhere. On phones and tablets there is no hover, so the first tap opens the preview as a bottom sheet instead of following the link: drag it down, tap the dim page or press Escape to close it, and the sheet's own link leads on. With reduced motion it only fades.
Hover Card is a Pro React component in the React overlay components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add hover-card, together with UIProvider, Drawer, Motion presets, useControllableState and useMediaQuery and the npm packages @radix-ui/react-hover-card, @radix-ui/react-slot and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add hover-cardHover Card is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import HoverCard from @/components/ui/hover-card.
import { HoverCard } from "@/components/ui/hover-card"
import { Avatar } from "@/components/ui/avatar"
export function Mention() {
return (
<p className="text-sm">
Ask{" "}
<HoverCard
trigger={
<a href="/profile/emma.carter" className="font-semibold underline decoration-foreground/25 underline-offset-4">
@emma.carter
</a>
}
sheetTitle="Emma Carter's profile"
>
<div className="flex items-center gap-3">
<Avatar src="/demo/ana-pop.jpg" name="Emma Carter" size="lg" status="online" className="[--avatar-ring:var(--popover)]" />
<div className="min-w-0">
<p className="text-[15px] font-semibold">Emma Carter</p>
<p className="text-[13px] text-muted-foreground">Portland · member since 2019</p>
</div>
</div>
<p className="mt-3 text-[13px] text-muted-foreground">48 listings · 4.9 ★ · replies in 1 h</p>
<a href="/profile/emma.carter" className="mt-3 inline-block font-medium underline underline-offset-4">
View profile
</a>
</HoverCard>{" "}
about the bike.
</p>
)
}Props
triggerReactNodeWhat you hover, rendered asChild: an <a>, a router Link, a Button or an avatar wrapped in a button. It must pass a ref to its element. Plain text gets a focusable button with a dotted underline, for a name or a term inside a sentence.
childrenReactNodeThe preview: an avatar row and stats, a listing with its photo, an article with its image. Round inner parts with rounded-(--hover-card-inner-radius), which follows radius and padding (and is 16px in the sheet). Put a real link to the destination in it for touch screens.
sheetTitledefault"Preview"stringThe bottom sheet's title for screen readers. The sheet shows no header, but a dialog needs a name: say what it previews, like "Profilul Anei Pop".
sizedefault"md""sm" | "md" | "lg" | "auto"Width of the floating card: 256, 288 or 384px, or auto to fit the content (192px at least). Never wider than the screen minus 12px a side. The sheet always spans the phone.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner radius of the card: 0, 12, 16 or 24px; full falls back to lg. The sheet keeps its own 28px top corners.
paddingdefault"md""none" | "sm" | "md" | "lg"Inner padding of the card: 0, 8, 16 or 20px. none lets a photo run to the edges (the card clips it to its corners), sm gives an inset photo with a matching smaller radius. The sheet always pads 20px.
arrowdefaultfalsebooleanA small arrow with a rounded tip in the card's color, pointing at the trigger. Its sides carry on the card's hairline and it keeps clear of the corners.
sidedefault"bottom""top" | "right" | "bottom" | "left"Where the card opens. It flips to the opposite side and slides along the edge when there is no room.
aligndefault"center""start" | "center" | "end"Alignment along the trigger's edge.
sideOffsetdefault8numberDistance from the trigger in px. The arrow adds its own 8px on top.
openDelaydefault500numberHover (or keyboard focus) time in ms before it opens, so the pointer can cross a page full of links without cards popping up. 0 opens at once.
closeDelaydefault200numberTime in ms before it closes after the pointer leaves the trigger and the card. It is the grace period for travelling onto the card; at 0 it closes before you get there.
responsivedefaulttruebooleanOn phones (below 768px) and touch screens (a coarse pointer, like an iPad at 1024px) the first tap on the trigger opens the preview as a bottom sheet instead of following the link. Off: the floating card everywhere, and a tap follows the link.
defaultOpendefaultfalsebooleanStarts open (uncontrolled). Leaving the trigger and the card, Escape or a click outside closes it.
disableddefaultfalsebooleanNever opens and closes if it is open, e.g. for your own name in a list. The trigger keeps working: a link navigates on click and on tap.
openbooleanControls it from outside, with onOpenChange.
onOpenChange(open: boolean) => voidCalled on hover, focus, a tap on touch screens, Escape and outside clicks. Not called for opens refused while disabled.
containerHTMLElement | nullWhere the card (or the sheet) mounts. document.body by default.
classNamesPartial<Record<"content" | "body" | "arrow", string>>Extra classes for the card (hover-card-content, also the sheet), its padded scroll region (hover-card-body) and the arrow svg (set a fill-* to recolor it).
classNamestringExtra classes for the card, merged last. Other Radix content props pass through: alignOffset, collisionPadding (12), avoidCollisions, sticky, hideWhenDetached, onEscapeKeyDown, onPointerDownOutside.
Parts
HoverCardRoot
componentCompound API: holds the state and picks the mode once (the Radix hover card, or the drawer's bottom sheet on phones and touch screens). No DOM. Props: { open, defaultOpen, onOpenChange, openDelay, closeDelay, responsive, disabled }.
HoverCardTrigger
componentAn <a> (or your child with asChild) that opens the card on hover and focus, or the sheet on a tap, where it keeps the link from navigating. A plain element gets data-slot="hover-card-trigger" and data-state; a component child keeps its own and gets data-hover-card-state. Props: ComponentProps<"a"> & { asChild?: boolean }.
HoverCardContent
componentThe portaled, animated card (hover-card-content with data-side, data-align, data-size, data-state) and its body (hover-card-body); in sheet mode the drawer's sheet with the same slots and a hidden title. Props: { side, align, sideOffset, size, radius, padding, arrow, sheetTitle, container, classNames } & Radix HoverCard.Content props.
useHoverCard
hookThe nearest hover card's state, for content that lays out bigger in the sheet (44px buttons, a larger avatar) or closes the card after an action. Signature: () => { open: boolean, setOpen: (open: boolean) => void, sheet: boolean }.
Examples
Listing preview
padding="sm" insets the photo, whose corners follow --hover-card-inner-radius. The price is formatted in USD.
Sides with the arrow
Four sides, each on a team avatar. size="auto" fits the text; the card flips when there is no room.
Instant vs default delays
Plain text triggers get a dotted underline. 0 / 0 opens at once and closes the moment you leave.
Link preview
padding="none" and size="lg": the cover runs to the card's edges and the card clips it.
On a phone
Touch screens have no hover, so the first tap opens a bottom sheet with 48px buttons (useHoverCard).
Dependencies
Files
- components/ui/hover-card.tsx
FAQ
Is the React Hover Card component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Hover Card?
Run npx wingo-ui@latest add hover-card. It also adds the Wingo UI items it builds on (UIProvider, Drawer, Motion presets, useControllableState and useMediaQuery) and the npm packages @radix-ui/react-hover-card, @radix-ui/react-slot and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/hover-card.
How does Hover Card behave on phones?
On phones (below 768px) and touch screens (a coarse pointer, like an iPad at 1024px) the first tap on the trigger opens the preview as a bottom sheet instead of following the link. Off: the floating card everywhere, and a tap follows the link. The responsive prop controls this and is on by default.