Photon Design System

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 / closeDelay live 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 (default false) — 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 router Link)
  • delay — number (default 600) — ms of hover before the card opens
  • closeDelay — number (default 300) — ms before it closes after the pointer leaves

PreviewCard.Positioner (<div>)

  • side — top · right · bottom · left (default bottom) — which edge of the link to place the card
  • align — start · center · end (default center) — alignment along that side
  • sideOffset — number (default 8) — gap between link and card
  • alignOffset — number (default 0) — shift along the alignment axis
  • collisionPadding — number (default 5) — 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 after closeDelay (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; Escape closes 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 Popover for 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 use Tooltip.