Skip to documentation content

Preview Card

Preview a linked destination on hover or focus without changing the link's behavior.

preview-card-demo

Overview

Use Preview Card to show a small summary when someone hovers or focuses a link: profiles, repositories, documents, issues, plans. The preview helps people decide whether the destination is worth opening — it never replaces the destination itself.

The component is honest about its role: the trigger is a real link and works exactly as it would without the card. Touch devices have no hover, so their users simply navigate; the preview is a progressive enhancement for pointer and keyboard contexts.

Anatomy

A preview card has two visible parts plus positioning internals:

  • PreviewCardTrigger renders an <a> element — your link — and manages the hover/focus timing.
  • PreviewCardPopup portals a positioned surface next to the trigger. It accepts side (bottom by default), align (center), and sideOffset (4 px), forwarding them to Base UI's positioner.
  • PreviewCardArrow optionally connects the popup visually to its trigger.
  • PreviewCardViewport is only needed when several triggers share one popup whose content swaps with an animated transition between them.

Inside the popup, content is yours: title, description, metadata, media. Keep it brief enough to scan in the moment before deciding to click.

Timing and interaction

The trigger waits 600 ms of continuous hover before opening (delay) and 300 ms after the pointer leaves before closing (closeDelay). The closeDelay lets people move from the link into the popup to read or copy text without it vanishing. Keyboard focus also waits for delay, so if you shorten it, remember you're shortening it for keyboard users too. Escape dismisses an open card, as does clicking elsewhere.

Both delays are tunable per trigger. Resist tuning the delay to zero: see Don't do this.

Accessibility

Because the trigger is a genuine anchor, everything about links holds: it is reachable by Tab, announced by its text, activatable with Enter, and supports middle-click, context menus, and copy-link. Opening the card changes none of that — treat the popup as supplementary information layered over navigation.

The popup appears on focus too, so keyboard users get the same summary pointer users see (after the shared delay). Content inside the popup should be readable but nonessential: nothing there may be the only place a fact or action exists, because touch users and people who click straight through will never see it.

Colors come from theme tokens, so popups adapt to dark mode automatically. Positioning supports logical sides (inline-start, inline-end) alongside physical ones, and the arrow mirrors accordingly, so cards work in right-to-left locales. Long titles wrap inside the fixed-width popup rather than overflowing it.

Installation

npx honestui@latest add preview-card

Usage

import {
  PreviewCard,
  PreviewCardPopup,
  PreviewCardTrigger,
} from "@/components/ui/preview-card";
<PreviewCard>
  <PreviewCardTrigger href="/projects/atlas">Atlas</PreviewCardTrigger>
  <PreviewCardPopup>
    Status, owner, and recent activity for Atlas
  </PreviewCardPopup>
</PreviewCard>

Give the trigger a real href; the preview is decoration on top of working navigation. HoverCard, HoverCardTrigger, and HoverCardContent remain available as aliases during migration.

Don't do this

Hiding essential actions inside the popup

// Bad
<PreviewCardPopup>
  <p>Aurora website</p>
  <Button onClick={deploy}>Deploy</Button>
</PreviewCardPopup>
// Good
<PreviewCardPopup>
  <p>Last deployed 2 hours ago · production</p>
</PreviewCardPopup>

Touch users can't hover, hasty users click through before the popup renders, and the card closes while they're mid-task — so a Deploy button that lives only here will be missed, half-seen, or fired accidentally. Put facts and actions where the task lives; the popup carries context only.

A preview trigger that doesn't navigate

// Bad
<PreviewCardTrigger onClick={(e) => e.preventDefault()}>
  Atlas project
</PreviewCardTrigger>
// Good
<PreviewCardTrigger href="/projects/atlas">
  Atlas project
</PreviewCardTrigger>

Once the trigger stops being a link, the whole contract breaks: keyboard users open a popup and then have nowhere to go, middle-click and copy-link stop working, and screen readers announce a dead end. If there's no destination worth linking, you wanted a Tooltip or Popover, not a Preview Card.

Removing the hover delay in dense lists

// Bad
{rows.map((row) => (
  <PreviewCardTrigger key={row.id} href={row.href} delay={0}>
    {row.name}
  </PreviewCardTrigger>
))}
// Good
<PreviewCardTrigger href={row.href} delay={600}>
  {row.name}
</PreviewCardTrigger>

Scanning a list means the pointer passes over many triggers. With no delay, every pass fires a popup and the page flickers through content nobody asked for — visual noise for pointer users and a barrage of announcements for screen-reader users sweeping the list. Keep the default delay wherever multiple previews sit near each other.

Examples

Repository preview

Name, one-line description, and star count — enough to decide whether to open the repo.

preview-card-repository

Profile preview

An avatar and role attached to a person's name anywhere it appears.

preview-card-profile

Product preview

Plan details surfaced next to the link that leads to billing.

preview-card-product

API reference

PartRendersNotes
PreviewCardnone (context root)Base UI Root props: open/defaultOpen, onOpenChange, actionsRef, handle
PreviewCardTrigger<a>href plus native anchor props; delay (600 ms) and closeDelay (300 ms)
PreviewCardPopuppositioned <div> via portalAdds side ("bottom"), align ("center"), sideOffset (4) forwarded to the positioner
PreviewCardArrowSVG arrowOptional; mirrors for logical sides
PreviewCardViewport<div>Only for multiple triggers sharing one animated popup

The trigger's payload prop and the root's render-function children let one shared popup render different summaries per trigger — pair that with PreviewCardViewport. HoverCard* names are aliases for compatibility.

See the Base UI Preview Card API.