Use Hover Card to preview the destination or entity behind a link. Keep the link useful without the card. Use Popover for an explicit interactive popup and Tooltip for a very short hint.
Live preview
import { ArrowUpRight } from "lucide-react";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
export default function HoverCardDemo() {
return (
<HoverCard>
<HoverCardTrigger
href="/docs"
className="inline-flex items-center gap-2 font-heading underline decoration-2 underline-offset-4"
>
Explore the component registry
<ArrowUpRight className="size-4" aria-hidden="true" />
</HoverCardTrigger>
<HoverCardContent className="w-80">
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-xs font-heading uppercase tracking-wide text-main-foreground/70">
neobrutal-ui
</p>
<h3 className="mt-1 text-lg font-heading">Editable React components</h3>
</div>
<span className="rounded-base border-2 border-border bg-main px-2 py-1 text-xs font-heading text-main-foreground">
Ready
</span>
</div>
<p className="mt-3 text-sm leading-6">
Install accessible Base UI components, themes, and templates through a shadcn-compatible
registry.
</p>
<div className="mt-4 grid grid-cols-3 gap-2 border-t-2 border-border pt-3 text-center text-xs">
<div>
<strong className="block text-base font-heading">Editable</strong>source
</div>
<div>
<strong className="block text-base font-heading">Light + dark</strong>themes
</div>
<div>
<strong className="block text-base font-heading">Next.js</strong>templates
</div>
</div>
</HoverCardContent>
</HoverCard>
);
}
#Installation
Complete the base setup. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/hover-card.jsonnpm install @base-ui/react@^1.8.0Create components/ui/hover-card.tsx:
"use client";
import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card";
import { cn } from "@/lib/utils";
type HoverCardContentProps = PreviewCardPrimitive.Popup.Props &
Pick<
PreviewCardPrimitive.Positioner.Props,
| "align"
| "alignOffset"
| "arrowPadding"
| "collisionAvoidance"
| "collisionBoundary"
| "collisionPadding"
| "disableAnchorTracking"
| "positionMethod"
| "side"
| "sideOffset"
| "sticky"
>;
function HoverCard(props: PreviewCardPrimitive.Root.Props) {
return <PreviewCardPrimitive.Root {...props} />;
}
function HoverCardTrigger({
closeDelay = 300,
delay = 700,
...props
}: PreviewCardPrimitive.Trigger.Props) {
return (
<PreviewCardPrimitive.Trigger
data-slot="hover-card-trigger"
closeDelay={closeDelay}
delay={delay}
{...props}
/>
);
}
function HoverCardContent({
align = "center",
alignOffset = 0,
arrowPadding = 0,
children,
className,
collisionAvoidance,
collisionBoundary,
collisionPadding = 0,
disableAnchorTracking,
positionMethod = "fixed",
side = "bottom",
sideOffset = 4,
sticky,
...props
}: HoverCardContentProps) {
return (
<PreviewCardPrimitive.Portal data-slot="hover-card-portal">
<PreviewCardPrimitive.Positioner
align={align}
alignOffset={alignOffset}
arrowPadding={arrowPadding}
collisionAvoidance={collisionAvoidance}
collisionBoundary={collisionBoundary}
collisionPadding={collisionPadding}
disableAnchorTracking={disableAnchorTracking}
positionMethod={positionMethod}
side={side}
sideOffset={sideOffset}
sticky={sticky}
className="isolate z-50"
>
<PreviewCardPrimitive.Popup
data-slot="hover-card-content"
{...props}
className={(state) =>
cn(
"z-50 w-64 origin-(--transform-origin) rounded-base border-2 border-border bg-main p-4 font-base text-main-foreground outline-hidden duration-100 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
typeof className === "function" ? className(state) : className,
)
}
>
{children}
</PreviewCardPrimitive.Popup>
</PreviewCardPrimitive.Positioner>
</PreviewCardPrimitive.Portal>
);
}
export { HoverCard, HoverCardTrigger, HoverCardContent };
#Usage
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
export default function DocumentationPreview() {
return (
<HoverCard>
<HoverCardTrigger href="/docs" className="underline">
Read the documentation
</HoverCardTrigger>
<HoverCardContent>
<p>Installation steps, component examples, and theme guidance.</p>
</HoverCardContent>
</HoverCard>
);
}#API reference
| Prop / part | Purpose | Default |
|---|---|---|
HoverCard.open / onOpenChange | Controlled preview visibility | Uncontrolled |
HoverCardTrigger.href / render | Link destination or composition | Anchor |
HoverCardContent.side / align | Preferred placement | bottom / center |
HoverCardContent.sideOffset | Trigger-to-card gap | 4 |
HoverCardContent.className | Width and layout | Styled surface |
HoverCardTrigger.delay / closeDelay | Hover opening and closing delay, in milliseconds | 700 / 300 |
HoverCardContent.alignOffset / arrowPadding / collisionPadding | Alignment, arrow, and collision-boundary spacing, in pixels | 0 |
HoverCardContent.positionMethod | CSS positioning method | fixed |
#Accessibility
Do not place required instructions or the only copy of an action inside hover-only content. Touch and keyboard interactions differ from hover, so provide an explicit alternative when opening the content matters. Use descriptive link text and avoid duplicate essential announcements for decorative preview text.