ComponentsOverlay
Hover card
A preview of what a link points to, such as a person or a record, shown while the pointer rests on it.
Import
import { HoverCard, HoverCardTrigger, HoverCardContent } from "@booleanpress/ui/hover-card"Usage
A hover card previews a link's target for sighted pointer and keyboard users, so they can decide whether to follow it. It adds nothing that is not reachable by following the link. For a short text hint on a control, use a tooltip; for content people act on, a popover.
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@booleanpress/ui/hover-card"
export function Assignee() {
return (
<HoverCard>
<HoverCardTrigger href="/team/maya">Maya Okafor</HoverCardTrigger>
<HoverCardContent>Support lead, billing queue.</HoverCardContent>
</HoverCard>
)
}The trigger is a link (a) unless you pass asChild and your own element. The card opens when the pointer rests on the trigger for openDelay (700 ms by default) or when the trigger receives keyboard focus, and closes closeDelay (300 ms) after the pointer leaves both, or when focus moves away. side and align place it; it flips when there is no room. It is uncontrolled, or controlled with open and onOpenChange.
Examples
Basic
A person's name previews their profile: an avatar, a role and the date they joined.
import { CalendarDaysIcon } from "lucide-react"
import { Avatar, AvatarFallback } from "@booleanpress/ui/avatar"
import { Button } from "@booleanpress/ui/button"
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@booleanpress/ui/hover-card"
export default function HoverCardBasic() {
return (
<p className="text-sm/normal text-muted-foreground">
Ticket assigned to{" "}
<HoverCard>
<HoverCardTrigger asChild>
<Button variant="link" className="h-auto p-0 align-baseline">
Maya Okafor
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-72">
<div className="flex gap-3">
<Avatar size="lg">
<AvatarFallback>MO</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1">
<p className="font-semibold text-foreground">Maya Okafor</p>
<p className="text-muted-foreground">Support lead, billing queue. Answers within two hours on weekdays.</p>
<p className="flex items-center gap-1.5 text-xs/normal text-muted-foreground">
<CalendarDaysIcon aria-hidden="true" className="size-3.5" />
Joined October 2026
</p>
</div>
</div>
</HoverCardContent>
</HoverCard>
</p>
)
}Placement
side puts the card above, after, below or before its trigger.
import { Button } from "@booleanpress/ui/button"
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@booleanpress/ui/hover-card"
const SIDES = [
{ side: "top", label: "Top" },
{ side: "right", label: "Right" },
{ side: "bottom", label: "Bottom" },
{ side: "left", label: "Left" },
] as const
export default function HoverCardPlacement() {
return (
<div className="flex flex-wrap justify-center gap-2">
{SIDES.map(({ side, label }) => (
<HoverCard key={side}>
<HoverCardTrigger asChild>
<Button variant="outline">{label}</Button>
</HoverCardTrigger>
<HoverCardContent side={side} className="w-56">
<p className="font-semibold text-foreground">Primary connection</p>
<p className="text-muted-foreground">Amazon SES, eu-west-1. Last test passed at 09:41.</p>
</HoverCardContent>
</HoverCard>
))}
</div>
)
}Delay
openDelay and closeDelay set how long the pointer must rest, and how long the card stays.
import { Button } from "@booleanpress/ui/button"
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@booleanpress/ui/hover-card"
const TIMINGS = [
{ label: "Instant", openDelay: 0, closeDelay: 0, text: "Opens and closes at once." },
{ label: "Default", openDelay: 700, closeDelay: 300, text: "Opens after 0.7 seconds and closes 0.3 seconds after the pointer leaves." },
{ label: "Slow", openDelay: 1500, closeDelay: 500, text: "Opens after 1.5 seconds and closes half a second after the pointer leaves." },
]
export default function HoverCardDelay() {
return (
<div className="flex flex-wrap justify-center gap-2">
{TIMINGS.map(({ label, openDelay, closeDelay, text }) => (
<HoverCard key={label} openDelay={openDelay} closeDelay={closeDelay}>
<HoverCardTrigger asChild>
<Button variant="outline">{label}</Button>
</HoverCardTrigger>
<HoverCardContent className="w-56">{text}</HoverCardContent>
</HoverCard>
))}
</div>
)
}Accessibility
- Semantics
- The trigger keeps its own role, a link by default. The card has no role and is not announced: screen readers do not read hover cards, which is why its content must also be reachable by following the link.
- Labels
- The trigger is named by its text. The card needs no name of its own.
- Focus
- Focus stays on the trigger while the card is open; the card's content is not in the tab order. Escape closes it.
- Known limits
- Touch screens have no hover: a tap follows the link. Where the browser also gives the trigger focus on a tap (Chrome on Android), the card opens as focus does, but a touch user never relies on it.
- Do not put buttons, links or form controls in the card: keyboard and screen-reader users cannot reach them.
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Focusing the trigger opens the card after openDelay; moving focus away closes it. |
| Escape | Closes the card; focus stays on the trigger. |
API
HoverCard
Renders Radix HoverCard.Root and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
closeDelay | number | Milliseconds after the pointer leaves before the card closes. 300 by default. | |
defaultOpen | boolean | Whether it starts open, when it controls itself. | |
onOpenChange | ((open: boolean) => void) | Called with true or false when it opens or closes. | |
open | boolean | Whether it is open, when you control it. Pair it with onOpenChange. | |
openDelay | number | Milliseconds the pointer rests on the trigger before the card opens. 700 by default. |
HoverCardTrigger
Renders Radix HoverCard.Trigger and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | Render the child element instead of a link, with the trigger's behaviour merged onto it. |
HoverCardContent
Renders Radix HoverCard.Content and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
align | "center" | "start" | "end" | start | Alignment along that side: start, center or end. center by default. |
alignOffset | number | ||
arrowPadding | number | ||
asChild | boolean | ||
avoidCollisions | boolean | ||
collisionBoundary | Boundary | Boundary[] | ||
collisionPadding | number | Partial<Record<"top" | "bottom" | "left" | "right", number>> | ||
forceMount | true | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. | |
hideWhenDetached | boolean | ||
onEscapeKeyDown | ((event: KeyboardEvent) => void) | Event handler called when the escape key is down. Can be prevented. | |
onFocusOutside | ((event: FocusOutsideEvent) => void) | Event handler called when the focus moves outside of the HoverCard.
Can be prevented. | |
onInteractOutside | ((event: FocusOutsideEvent | PointerDownOutsideEvent) => void) | Event handler called when an interaction happens outside the HoverCard.
Specifically, when a pointerdown event happens outside or focus moves outside of it.
Can be prevented. | |
onPointerDownOutside | ((event: PointerDownOutsideEvent) => void) | Event handler called when the a pointerdown event happens outside of the HoverCard.
Can be prevented. | |
side | "top" | "bottom" | "left" | "right" | bottom | The preferred side: top, right, bottom or left. bottom by default; it flips when there is no room. |
sideOffset | number | Distance in pixels from the trigger. 4 by default. | |
sticky | "always" | "partial" | ||
updatePositionStrategy | "always" | "optimized" |
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: data-slot="hover-card" (HoverCard), data-slot="hover-card-trigger" (HoverCardTrigger), data-slot="hover-card-portal" (HoverCardContent).
Theming
The card is a floating surface, as the popover: --popover and --popover-foreground, a --border edge, an 8px radius and a soft shadow. The theme's overlay motion opens and closes it.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--popover | background |
--popover-foreground | text |