# 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"`
- **Radix Hover Card:** <https://www.radix-ui.com/primitives/docs/components/hover-card>
- **Page:** <https://ui.booleanpress.com/components/hover-card> · @booleanpress/ui 0.2.0

## 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](/components/tooltip); for content people act on, a [popover](/components/popover).

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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.

| Token | Used for |
| --- | --- |
| `--popover` | background |
| `--popover-foreground` | text |
