Skip to the content

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

Keyboard
KeyBehaviour
TabFocusing the trigger opens the card after openDelay; moving focus away closes it.
EscapeCloses the card; focus stays on the trigger.

API

HoverCard

Renders Radix HoverCard.Root and passes it every other prop.

HoverCard props
PropTypeDefaultDescription
closeDelaynumberMilliseconds after the pointer leaves before the card closes. 300 by default.
defaultOpenbooleanWhether it starts open, when it controls itself.
onOpenChange((open: boolean) => void)Called with true or false when it opens or closes.
openbooleanWhether it is open, when you control it. Pair it with onOpenChange.
openDelaynumberMilliseconds the pointer rests on the trigger before the card opens. 700 by default.

HoverCardTrigger

Renders Radix HoverCard.Trigger and passes it every other prop.

HoverCardTrigger props
PropTypeDefaultDescription
asChildbooleanRender 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.

HoverCardContent props
PropTypeDefaultDescription
align"center" | "start" | "end"startAlignment along that side: start, center or end. center by default.
alignOffsetnumber
arrowPaddingnumber
asChildboolean
avoidCollisionsboolean
collisionBoundaryBoundary | Boundary[]
collisionPaddingnumber | Partial<Record<"top" | "bottom" | "left" | "right", number>>
forceMounttrueUsed to force mounting when more control is needed. Useful when controlling animation with React animation libraries.
hideWhenDetachedboolean
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"bottomThe preferred side: top, right, bottom or left. bottom by default; it flips when there is no room.
sideOffsetnumberDistance 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.

Theme tokens
TokenUsed for
--popoverbackground
--popover-foregroundtext