Preview Link Card

A link that shows a screenshot of the page it leads to when hovered, in a card that links there too. Built on React Aria's PreviewTrigger and Popover.

Read the — hover to preview, click to dive in.

"use client"

import { cn } from "cn"
import {
  PreviewLinkCard as PreviewLinkCardPrimitive,
  PreviewLinkCardImage as PreviewLinkCardImagePrimitive,
  PreviewLinkCardPopover as PreviewLinkCardPopoverPrimitive,
  PreviewLinkCardTrigger as PreviewLinkCardTriggerPrimitive,
  type PreviewLinkCardImageProps,
  type PreviewLinkCardPopoverProps,
  type PreviewLinkCardProps,
  type PreviewLinkCardTriggerProps,
} from "@animate-ui/react/primitives/react-aria/preview-link-card"

function PreviewLinkCard(props: PreviewLinkCardProps) {
  return <PreviewLinkCardPrimitive {...props} />
}

function PreviewLinkCardTrigger(props: PreviewLinkCardTriggerProps) {
  return <PreviewLinkCardTriggerPrimitive {...props} />
}

function PreviewLinkCardContent({
  className,
  placement = "top",
  offset = 4,
  ...props
}: Omit<PreviewLinkCardPopoverProps, "className"> & { className?: string }) {
  return (
    <PreviewLinkCardPopoverPrimitive
      placement={placement}
      offset={offset}
      className={cn(
        "ring-foreground/10 bg-popover z-50 origin-(--trigger-anchor-point) overflow-hidden rounded-lg shadow-md ring-1 outline-hidden",
        className
      )}
      {...props}
    />
  )
}

function PreviewLinkCardImage({
  className,
  ...props
}: PreviewLinkCardImageProps) {
  return (
    <PreviewLinkCardImagePrimitive
      className={cn("block", className)}
      {...props}
    />
  )
}

export {
  PreviewLinkCard,
  PreviewLinkCardTrigger,
  PreviewLinkCardContent,
  PreviewLinkCardImage,
}

function toPlacement(
  side: "top" | "right" | "bottom" | "left",
  align: "start" | "center" | "end"
) {
  if (align === "center") return side
  if (side === "top" || side === "bottom") return `${side} ${align}` as const
  return `${side} ${align === "start" ? "top" : "bottom"}` as const
}

export default function PreviewLinkCardDemo({
  href = "https://animate-ui.com/docs",
  side = "top",
  sideOffset = 4,
  align = "center",
  alignOffset = 0,
}: {
  href?: string
  side?: "top" | "right" | "bottom" | "left"
  sideOffset?: number
  align?: "start" | "center" | "end"
  alignOffset?: number
}) {
  return (
    <p className="text-muted-foreground">
      Read the{" "}
      <PreviewLinkCard href={href}>
        <PreviewLinkCardTrigger
          target="_blank"
          className="text-foreground underline"
        >
          Animate UI Docs
        </PreviewLinkCardTrigger>
        <PreviewLinkCardContent
          placement={toPlacement(side, align)}
          offset={sideOffset}
          crossOffset={alignOffset}
          target="_blank"
        >
          <PreviewLinkCardImage alt="Animate UI Docs" />
        </PreviewLinkCardContent>
      </PreviewLinkCard>{" "}
      — hover to preview, click to dive in.
    </p>
  )
}

Installation

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

It's built on React Aria Components, a peer dependency of this primitive:

npm install react-aria-components
pnpm add react-aria-components
yarn add react-aria-components
bun add react-aria-components

Usage

import {
  PreviewLinkCard,
  PreviewLinkCardImage,
  PreviewLinkCardPopover,
  PreviewLinkCardTrigger,
} from '@animate-ui/react/primitives/react-aria/preview-link-card';

export function DocsLink() {
  return (
    <PreviewLinkCard href="https://animate-ui.com/docs">
      <PreviewLinkCardTrigger>Animate UI Docs</PreviewLinkCardTrigger>
      <PreviewLinkCardPopover>
        <PreviewLinkCardImage alt="Animate UI Docs" />
      </PreviewLinkCardPopover>
    </PreviewLinkCard>
  );
}

Animate UI v1 had no React Aria version: this one has the parts of the Radix UI and Base UI ones, the React Aria way. The Root is a PreviewTrigger (delay, closeDelay, isOpen…), the trigger a Link, and the card a Popover (placement, offset…), which portals itself: there is no Portal part.

The screenshot comes from Microlink's API, as in Animate UI v1: it captures href on a mobile viewport three times the image's size. Pass src to show an image of your own instead. Either way, it's preloaded (a <link rel="preload"> in the head) as soon as the card mounts, so it's there when the card opens.

Animation

The card is Animate UI's Popover under a PreviewTrigger, so it moves exactly like the Hover Card:

  • Popover grows from 0.8 and fades in, from the link (--trigger-anchor-point), on a spring (stiffness 400, damping 29) that overshoots a little before it settles.
  • It opens after the delay (600ms by default) and, once the closeDelay is over (200ms), closes the same way: it shrinks back to 0.8 on the same spring while it fades out, then unmounts.
  • Moving straight from one preview to the next during PreviewTrigger's warmup swaps them without animation, as React Aria intends.
  • React Aria opens it on hover for pointer input, after a delay on keyboard focus, and on a long press: nothing sticks on touch screens.
  • Under reduced motion, it only fades (no scale).

API Reference

PreviewLinkCard

React Aria's PreviewTrigger props (delay, closeDelay, isOpen, onOpenChange…), plus:

PropTypeDefault
href
string-
src?
string-
width?
number240
height?
number135
deviceScaleFactor?
number1
colorScheme?
'light' | 'dark''light'

PreviewLinkCardTrigger

React Aria's Link props: a link, to the Root's href unless it has its own.

PreviewLinkCardPopover

Animate UI's Popover (transition, exitTransition, placement…), filled with a Link to the Root's href: the Popover is the card, and its className styles it.

PropTypeDefault
placement?
Placement'top'
offset?
number10
href?
string-
target, rel, download, hrefLang, ping, referrerPolicy?
LinkProps-

PreviewLinkCardImage

PropTypeDefault
alt?
string'preview image'
...props?
Omit<React.ComponentProps<'img'>, 'src' | 'width' | 'height'>-

usePreviewLinkCard

Returns the card's href, its image's src, width and height: for an image of your own.

Credits

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page