---
import { buttonVariants } from "@/components/ui/button"
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card"
---
<HoverCard delay={10} closeDelay={100}>
<HoverCardTrigger class={buttonVariants({ variant: "link" })}>
Hover Here
</HoverCardTrigger>
<HoverCardContent class="flex w-64 flex-col gap-0.5">
<div class="font-semibold">@nextjs</div>
<div>The React Framework – created and maintained by @vercel.</div>
<div class="mt-1 text-xs text-muted-foreground">Joined December 2021</div>
</HoverCardContent>
</HoverCard>Installation
npx shadcn@latest add @fulldev/hover-card
Usage
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card"
<HoverCard>
<HoverCardTrigger>Hover</HoverCardTrigger>
<HoverCardContent>
The React Framework – created and maintained by @vercel.
</HoverCardContent>
</HoverCard>
HoverCardTrigger renders a button, or an a with href. Style it as a
button with buttonVariants, such as
class={buttonVariants({ variant: "link" })}.
Composition
HoverCard
├── HoverCardTrigger
└── HoverCardContent
Trigger Delays
Set delay and closeDelay (in ms) on HoverCard, not on the trigger.
<HoverCard delay={100} closeDelay={200}>
<HoverCardTrigger>Hover</HoverCardTrigger>
<HoverCardContent>Content</HoverCardContent>
</HoverCard>
Positioning
Use the side and align props on HoverCardContent to control placement.
<HoverCard>
<HoverCardTrigger>Hover</HoverCardTrigger>
<HoverCardContent side="top" align="start">
Content
</HoverCardContent>
</HoverCard>
Basic
---
import { buttonVariants } from "@/components/ui/button"
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card"
---
<HoverCard delay={10} closeDelay={100}>
<HoverCardTrigger class={buttonVariants({ variant: "link" })}>
Hover Here
</HoverCardTrigger>
<HoverCardContent class="flex w-64 flex-col gap-0.5">
<div class="font-semibold">@nextjs</div>
<div>The React Framework – created and maintained by @vercel.</div>
<div class="mt-1 text-xs text-muted-foreground">Joined December 2021</div>
</HoverCardContent>
</HoverCard>Sides
---
import { buttonVariants } from "@/components/ui/button"
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card"
const HOVER_CARD_SIDES = ["left", "top", "bottom", "right"] as const
---
<div class="flex flex-wrap justify-center gap-2">
{HOVER_CARD_SIDES.map((side) => (
<HoverCard delay={100} closeDelay={100}>
<HoverCardTrigger
class={buttonVariants({ variant: "outline", class: "capitalize" })}
>
{side}
</HoverCardTrigger>
<HoverCardContent side={side}>
<div class="flex flex-col gap-1">
<h4 class="font-medium">Hover Card</h4>
<p>This hover card appears on the {side} side of the trigger.</p>
</div>
</HoverCardContent>
</HoverCard>
))}
</div>RTL
side takes physical sides only (top, right, bottom, left), so the
example resolves inline-start and inline-end from dir.
---
import { buttonVariants } from "@/components/ui/button"
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card"
const translations = {
en: {
dir: "ltr",
values: {
trigger: "Wireless Headphones",
name: "Wireless Headphones",
price: "$99.99",
"inline-start": "Inline Start",
left: "Left",
top: "Top",
bottom: "Bottom",
right: "Right",
"inline-end": "Inline End",
},
},
ar: {
dir: "rtl",
values: {
trigger: "سماعات لاسلكية",
name: "سماعات لاسلكية",
price: "٩٩.٩٩ $",
"inline-start": "بداية السطر",
left: "يسار",
top: "أعلى",
bottom: "أسفل",
right: "يمين",
"inline-end": "نهاية السطر",
},
},
he: {
dir: "rtl",
values: {
trigger: "אוזניות אלחוטיות",
name: "אוזניות אלחוטיות",
price: "99.99 $",
"inline-start": "תחילת השורה",
left: "שמאל",
top: "למעלה",
bottom: "למטה",
right: "ימין",
"inline-end": "סוף השורה",
},
},
} as const
const physicalSides = ["left", "top", "bottom", "right"] as const
const logicalSides = ["inline-start", "inline-end"] as const
const { dir, values: t } = translations.ar
// HoverCard takes physical sides only: resolve the logical ones for `dir`.
const physicalSide = (side: (typeof logicalSides)[number]) =>
(side === "inline-start") === (dir === "rtl") ? "right" : "left"
---
<div class="grid gap-4">
<div class="flex flex-wrap justify-center gap-2">
{physicalSides.map((side) => (
<HoverCard delay={10} closeDelay={100}>
<HoverCardTrigger class={buttonVariants({ variant: "outline" })}>
{t[side]}
</HoverCardTrigger>
<HoverCardContent
side={side}
class="flex w-64 flex-col gap-1"
dir={dir}
>
<div class="font-semibold">{t.name}</div>
<div class="text-sm text-muted-foreground">{t.price}</div>
</HoverCardContent>
</HoverCard>
))}
</div>
<div class="flex flex-wrap justify-center gap-2">
{logicalSides.map((side) => (
<HoverCard delay={10} closeDelay={100}>
<HoverCardTrigger class={buttonVariants({ variant: "outline" })}>
{t[side]}
</HoverCardTrigger>
<HoverCardContent
side={physicalSide(side)}
class="flex w-64 flex-col gap-1"
dir={dir}
>
<div class="font-semibold">{t.name}</div>
<div class="text-sm text-muted-foreground">{t.price}</div>
</HoverCardContent>
</HoverCard>
))}
</div>
</div>Open state
Use defaultOpen for an initially open card that still responds to pointer and
keyboard interactions. The open prop enables controlled mode: interactions
request changes but do not update the state themselves. Dispatch hover-card:set
with { detail: { open: false } } (or true) on the root to update it.
Content mounting
Data Slot’s recommended "lazy" strategy is the default: closed content is
retained but detached from the document. Fulldev initializes nested Data Slot
components inside that retained content too.
Use mountStrategy="eager" when an integration needs to query closed content
through the document. Both strategies include content in server-rendered HTML;
neither adds a no-JavaScript fallback.
API Reference
Source: src/components/ui/hover-card
HoverCard prop | Type | Default |
|---|---|---|
delay | number | 700 |
closeDelay | number | 300 |
skipDelayDuration | number | 300 |
defaultOpen | boolean | |
open | boolean | |
side | "top" | "right" | "bottom" | "left" | "bottom" |
align | "start" | "center" | "end" | "center" |
sideOffset | number | 4 |
alignOffset | number | 0 |
avoidCollisions | boolean | true |
collisionPadding | number | 8 |
portal | boolean | true |
closeOnClickOutside | boolean | true |
closeOnEscape | boolean | true |
mountStrategy | "lazy" | "eager" | "lazy" |
HoverCardContent takes side, align, sideOffset, alignOffset,
avoidCollisions and collisionPadding to override the root per content.
See the Data Slot docs
for events and behavior.