Preview Card
A hover-triggered card that previews what’s behind a link — a profile, a repo,
an article. Built on Base UI PreviewCard. Like a Tooltip it opens on
hover/focus, but it holds richer (still non-essential) content.
Import
import { PreviewCard } from "@photon-ai/pho-ui/components/preview-card";
Basic
Wrap an inline link with PreviewCard.Trigger (pass the link through render);
the card opens after a short hover and the link stays fully clickable.
Built and maintained by @photon.
<PreviewCard.Root>
<PreviewCard.Trigger render={<a href="/photon">@photon</a>} />
<PreviewCard.Portal>
<PreviewCard.Positioner>
<PreviewCard.Popup>
<Avatar.Root>…</Avatar.Root>
<div>Photon — the deployment platform.</div>
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
</PreviewCard.Root>
Placement
PreviewCard.Positioner sets where the card sits: side picks the edge
(bottom by default), align sets alignment along it, and sideOffset is the
gap — defaulted to 8 here. Placement flips automatically when the chosen side
would overflow the viewport.
Read the changelog to see what shipped.
<PreviewCard.Positioner side="top">
<PreviewCard.Popup>…</PreviewCard.Popup>
</PreviewCard.Positioner>
Anatomy
- PreviewCard.Root — owns open state;
delay/closeDelaylive on the trigger - PreviewCard.Trigger — the inline link being previewed (renders an
<a>) - PreviewCard.Portal → Positioner → Popup — the floating preview
Props
Every part also accepts className and render (Base UI’s prop for swapping
the underlying element).
PreviewCard.Root (holds state; renders no element)
- open —
boolean— controlled open state - defaultOpen —
boolean(defaultfalse) — initial open state when uncontrolled - onOpenChange —
(open, details) => void— fires when the card opens or closes - onOpenChangeComplete —
(open) => void— fires after the open/close animation settles
PreviewCard.Trigger (<a>)
- render — element to render as the trigger (an
<a>or a routerLink) - delay —
number(default600) — ms of hover before the card opens - closeDelay —
number(default300) — ms before it closes after the pointer leaves
PreviewCard.Positioner (<div>)
- side —
top·right·bottom·left(defaultbottom) — which edge of the link to place the card - align —
start·center·end(defaultcenter) — alignment along that side - sideOffset —
number(default8) — gap between link and card - alignOffset —
number(default0) — shift along the alignment axis - collisionPadding —
number(default5) — space kept from the viewport edge
PreviewCard.Popup (<div>) takes only the common className / render.
Accessibility
- Semantics — the trigger renders a real
<a>, and the popup carries no ARIA role and is not wired to the link as a label or description. The preview is a purely visual enhancement, so the link text must stand on its own — never hide anything essential inside the card. - Opening — opens on hover or keyboard focus of the link after
delay(600 ms); closes on blur or pointer-leave aftercloseDelay(300 ms). The pointer can travel from the link onto the card without dismissing it. - Keyboard — the link is an ordinary tab stop and activates with
Enter;Escapecloses an open card. - Focus — focus stays on the link and never moves into the popup, so opening
a card doesn’t disturb tab order. Don’t put focusable controls in the preview
— keyboard and touch users can’t reach them; use
Popoverfor that.
Best practices
- Preview content that helps the user decide whether to follow the link — an avatar, a name, a one-line summary. Don’t duplicate the whole destination.
- Keep the link itself meaningful and clickable without ever opening the card; the preview is an enhancement, not a requirement.
- Keep the card non-interactive — no buttons or forms inside. For click-triggered,
interactive content use
Popover; for a plain text hint useTooltip.