Skip to content

Link Preview

A horizontal link preview with an image, seller details, and optional product pricing and ratings.

Usage

import { LinkPreview } from "@/components/ai/link-preview"


export function Example() {
  return <LinkPreview url="https://www.amazon.com/s?k=eau+de+parfum" title="Eau de Parfum" siteName="Amazon.com" price="$75.65" rating={4.7} reviewCount="38K" />
}

Install

pnpm dlx shadcn@latest add @pastaui/link-preview

Source

import { StarIcon } from '@solar-icons/react/bold/star'
import { GlobalIcon as Globe2Icon } from '@solar-icons/react/linear/global'
import * as React from 'react'


import { cn } from 'cn'


interface LinkPreviewProps extends React.ComponentProps<'article'> {
  description?: string
  imageSrc?: string
  price?: string
  seller?: string
  rating?: number
  reviewCount?: string
  onOpen?: () => void
  siteName?: string
  title: string
  url: string
}


function LinkPreview({
  className,
  description,
  imageSrc,
  price,
  seller,
  rating,
  reviewCount,
  onOpen,
  siteName,
  title,
  url,
  ...props
}: LinkPreviewProps) {
  const [imageLoaded, setImageLoaded] = React.useState(false)
  let hostname = url
  try {
    hostname = new URL(url).hostname.replace('www.', '')
  } catch {
    hostname = url
  }


  return (
    <article
      data-slot="link-preview"
      className={cn(
        'flex items-center gap-4 rounded-[16px] font-sans text-sm leading-5 font-[450] tracking-[-0.05px] text-muted-foreground sm:gap-6',
        className,
      )}
      {...props}
    >
      <div className="flex size-24 shrink-0 items-center justify-center overflow-hidden rounded-[16px] bg-muted sm:size-40">
        {imageSrc ? (
          <img
            alt=""
            className={cn(
              'size-full object-cover transition-opacity duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]',
              !imageLoaded && 'opacity-0',
            )}
            height={160}
            loading="lazy"
            onLoad={() => setImageLoaded(true)}
            ref={(image) => {
              if (image?.complete) setImageLoaded(true)
            }}
            src={imageSrc}
            width={160}
          />
        ) : (
          <Globe2Icon className="size-6 text-muted-foreground" />
        )}
      </div>
      <div className="flex min-w-0 flex-1 flex-col gap-2">
        <h3 className="card-heading leading-6 text-foreground">
          <a
            href={url}
            target="_blank"
            rel="noopener noreferrer"
            className="rounded-sm outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
            onClick={(event) => {
              if (onOpen) {
                event.preventDefault()
                onOpen()
              }
            }}
          >
            {title}
          </a>
        </h3>
        <p className="text-muted-foreground">
          {price && (
            <>
              <span className="tabular-nums">{price}</span>
              <span aria-hidden="true"> · </span>
            </>
          )}
          {siteName ?? hostname}
          {seller && ` – ${seller}`}
        </p>
        {rating !== undefined && (
          <p
            className="flex items-center gap-2 text-muted-foreground"
            aria-label={`${rating} out of 5 stars${reviewCount ? `, ${reviewCount} reviews` : ''}`}
          >
            <StarIcon aria-hidden="true" className="size-4 shrink-0" />
            <span>
              {rating.toFixed(1)}
              {reviewCount && ` (${reviewCount})`}
            </span>
          </p>
        )}
        {description && (
          <p className="line-clamp-2 text-xs leading-5 text-muted-foreground">{description}</p>
        )}
      </div>
    </article>
  )
}


export { LinkPreview }
export type { LinkPreviewProps }


Props

Link Preview props
PropTypeDefaultDescription
urlstring= —Identifies the destination and displayed hostname.
imageSrcstring= —Displays an optional preview image.
pricestring= —Displays a formatted price, including its currency.
sellerstring= —Displays the seller beside the site name.
ratingnumber= —Displays a star rating out of five.
reviewCountstring= —Displays the formatted review count beside the rating.
onOpen() => void= —Opens the destination.