Skip to content

Hover Card

A preview of what's behind a link, such as a person, a repository or a page, shown when the link is hovered or focused.

SourceBase UIRegistry

Reviewed by @grace 2 hours ago

import { CalendarDaysIcon, MapPinIcon } from "lucide-react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"

export default function HoverCardDemo() {
  return (
    <p className="text-sm text-muted-foreground">
      Reviewed by{" "}
      <HoverCard>
        <HoverCardTrigger
          href="#"
          className="font-medium text-foreground underline decoration-current/30 underline-offset-4 hover:decoration-current"
        >
          @grace
        </HoverCardTrigger>
        <HoverCardContent className="w-80">
          <div className="flex gap-4">
            <Avatar size="lg">
              <AvatarFallback>GH</AvatarFallback>
            </Avatar>
            <div className="flex min-w-0 flex-1 flex-col gap-2">
              <div className="flex items-start justify-between gap-2">
                <div className="flex flex-col">
                  <span className="text-sm font-semibold text-foreground">
                    Grace Hopper
                  </span>
                  <span className="text-xs text-muted-foreground">@grace</span>
                </div>
                <Button size="xs" variant="outline">
                  Follow
                </Button>
              </div>
              <p className="text-sm leading-5 text-foreground">
                Compiler engineer. Design systems, developer tools and very
                small nanoseconds.
              </p>
              <div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
                <span className="flex items-center gap-1">
                  <MapPinIcon className="size-3.5" />
                  Arlington, VA
                </span>
                <span className="flex items-center gap-1">
                  <CalendarDaysIcon className="size-3.5" />
                  Joined March 2019
                </span>
              </div>
            </div>
          </div>
        </HoverCardContent>
      </HoverCard>{" "}
      2 hours ago
    </p>
  )
}

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/hover-card.json

Usage

import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"
<HoverCard>
  <HoverCardTrigger href="/people/grace">@grace</HoverCardTrigger>
  <HoverCardContent>{/* profile preview */}</HoverCardContent>
</HoverCard>

HoverCardTrigger renders a real <a>: the preview is an enhancement of a link that works on its own. It's built on the Base UI Preview Card.

Examples

Issue preview

References to issues, pull requests and documents are good candidates: the card answers what is this and is it done? without a click. Keep it to identity, status, and one or two lines of context.

Fixed in DAT-1284, shipped in v1.4.2.

import { CircleDotIcon, MessageSquareIcon } from "lucide-react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"

export default function HoverCardIssuePreview() {
  return (
    <p className="max-w-md text-center text-sm leading-6 text-muted-foreground">
      Fixed in{" "}
      <HoverCard>
        <HoverCardTrigger
          href="#"
          className="font-mono font-medium text-link underline decoration-current/30 underline-offset-4 hover:decoration-current"
        >
          DAT-1284
        </HoverCardTrigger>
        <HoverCardContent className="flex w-80 flex-col gap-3">
          <div className="flex items-center justify-between gap-2">
            <span className="flex items-center gap-1.5 font-mono text-xs text-muted-foreground">
              <CircleDotIcon className="size-3.5 text-success-11" />
              DAT-1284
            </span>
            <Badge variant="success">
              <BadgeDot />
              Done
            </Badge>
          </div>
          <p className="text-sm leading-5 font-medium text-foreground">
            Date picker traps focus when opened from a popover
          </p>
          <div className="flex items-center justify-between text-xs text-muted-foreground">
            <span className="flex items-center gap-2">
              <Avatar size="xs">
                <AvatarFallback>LT</AvatarFallback>
              </Avatar>
              Linus Torvalds · closed 2 days ago
            </span>
            <span className="flex items-center gap-1 tabular-nums">
              <MessageSquareIcon className="size-3.5" />
              14
            </span>
          </div>
        </HoverCardContent>
      </HoverCard>
      , shipped in v1.4.2.
    </p>
  )
}

Preview where an external link leads before people commit to leaving the page. Here the card opens above the link (side="top") and uses a tinted footer for metadata.

prfct is built on mui/base-ui, the unstyled primitives from the teams behind Radix, Floating UI and Material UI.

import { GitForkIcon, StarIcon } from "lucide-react"

import { Badge } from "@/components/ui/badge"
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"

export default function HoverCardLinkPreview() {
  return (
    <p className="max-w-md text-center text-sm leading-6 text-muted-foreground">
      prfct is built on{" "}
      <HoverCard>
        <HoverCardTrigger
          href="https://github.com/mui/base-ui"
          target="_blank"
          rel="noreferrer"
          className="font-medium text-link underline decoration-current/30 underline-offset-4 hover:decoration-current"
        >
          mui/base-ui
        </HoverCardTrigger>
        <HoverCardContent side="top" className="w-80 p-0">
          <div className="flex flex-col gap-2 p-4">
            <div className="flex items-center gap-2">
              <span className="text-sm font-semibold text-foreground">
                mui/base-ui
              </span>
              <Badge variant="outline" size="sm">
                Public
              </Badge>
            </div>
            <p className="text-sm leading-5 text-muted-foreground">
              Unstyled UI components for building accessible web apps and design
              systems.
            </p>
          </div>
          <div className="flex items-center gap-4 border-t bg-surface px-4 py-2.5 text-xs text-muted-foreground">
            <span className="flex items-center gap-1.5">
              <span className="size-2.5 rounded-full bg-blue-9" />
              TypeScript
            </span>
            <span className="flex items-center gap-1 tabular-nums">
              <StarIcon className="size-3.5" />
              7.9k
            </span>
            <span className="flex items-center gap-1 tabular-nums">
              <GitForkIcon className="size-3.5" />
              410
            </span>
          </div>
        </HoverCardContent>
      </HoverCard>
      , the unstyled primitives from the teams behind Radix, Floating UI and
      Material UI.
    </p>
  )
}

Delay

delay (600ms by default) and closeDelay (300ms) on the trigger decide how intentional a hover must be. Long delays prevent cards from flashing as the pointer crosses a page; short ones suit dense lists that people scan on purpose.

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card"

const people = [
  {
    name: "Ada Lovelace",
    initials: "AL",
    role: "Analytical engines",
    delay: 150,
  },
  { name: "Alan Turing", initials: "AT", role: "Computability", delay: 600 },
  {
    name: "Katherine Johnson",
    initials: "KJ",
    role: "Orbital mechanics",
    delay: 1000,
  },
]

export default function HoverCardDelay() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-4">
      {people.map((person) => (
        <HoverCard key={person.name}>
          <HoverCardTrigger
            href="#"
            delay={person.delay}
            closeDelay={150}
            className="flex flex-col items-center gap-2 rounded-lg p-2 text-sm text-foreground focus-ring hover:bg-accent"
          >
            <Avatar size="lg">
              <AvatarFallback>{person.initials}</AvatarFallback>
            </Avatar>
            <span className="font-mono text-xs text-muted-foreground">
              {person.delay}ms
            </span>
          </HoverCardTrigger>
          <HoverCardContent className="w-60">
            <p className="text-sm font-semibold text-foreground">
              {person.name}
            </p>
            <p className="text-sm text-muted-foreground">{person.role}</p>
          </HoverCardContent>
        </HoverCard>
      ))}
    </div>
  )
}

Guidelines

When to use

  • To preview the destination of a link: a person, an issue, a repository, a document.
  • When the preview helps people decide whether to navigate, without making them navigate.

When not to use

  • For essential information — hover cards are invisible on touch devices and easy to miss. Put it on the page.
  • For interactive tasks — use a Popover, which opens on click and can hold forms.
  • For a one-line label — use a Tooltip.

A preview, not a page

The card should confirm this is what you're looking for. If you find yourself adding tabs, lists or several actions, link to a page instead.

GH
Grace HopperCompiler engineer · Arlington
DoIdentity and a line of context, then get out of the way.
Grace Hopper
Don’tA dashboard in a hover card disappears the moment the pointer slips.

Accessibility

A hover card is a visual enhancement for sighted pointer and keyboard users. It opens when the link is hovered and when it receives keyboard focus, but it isn't announced and the link doesn't reference it, so screen reader users experience the link on its own. Treat the card as optional context.

KeyBehavior
Tab
Focusing the link opens the card after the delay.
Enter
Follows the link.
Esc
Closes the card.
  • The link must stand on its own. Its text should say where it goes; the card only adds context.
  • Don't hide essentials in it. Anything people must know or do can't live only in a hover card.
  • Hoverable. Moving the pointer from the link onto the card keeps it open, within the closeDelay.

API reference

HoverCard

The root. Doesn't render an element. Accepts every prop of Base UI PreviewCard.Root.

PropTypeDefault
open

Whether the card is open. Use with onOpenChange to control it.

booleanNo default
defaultOpen

Whether it is open initially, when uncontrolled.

booleanfalse
onOpenChange

Called when it opens or closes.

(open: boolean, details) => voidNo default
actionsRef

Imperative handle to close or unmount the card.

RefObject<{ close, unmount }>No default

HoverCardTrigger

The link that opens the card. Renders an <a> and accepts every anchor attribute.

PropTypeDefault
hrefrequired

Where the link goes. The card is a preview of this destination.

stringNo default
delay

How long the pointer must rest on the link before the card opens, in ms.

number600
closeDelay

How long to wait before closing after the pointer leaves, in ms.

number300

HoverCardContent

Renders the portal, positioner and popup.

PropTypeDefault
side

Preferred side of the link. Flips when it doesn't fit.

"top" | "right" | "bottom" | "left" | "inline-start" | "inline-end""bottom"
sideOffset

Distance from the link, in px.

number6
align

Alignment against the link.

"start" | "center" | "end""center"
alignOffset

Offset along the alignment axis, in px.

number0
className

Merged with the popup classes. The default width is w-72.

stringNo default