Link preview

Links shown with their content: a card with a loading skeleton, a hover card on a link in a reply, a pasted link in the composer, and rich embeds for videos, repos and posts.

Loading preview…

Install

  1. 1

    Run this command in your Tailwind CSS project. It adds components/ai/link-preview-default.tsx and installs its dependencies.

    $ npx shadcn@latest add https://gradientdeck.com/ai-components/r/link-preview-default.json

Usage & API

Example

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

<LinkPreview
  link={link}
/>

Props LinkPreview

PropType & descriptionRequired
linkLinkMetaRequired
loadingbooleanOptional
asLinkbooleanRender as a plain card instead of a link, e.g. when it already sits inside one.Optional
imageReactNodeThe page's preview image. Falls back to a drawn tile from the domain.Optional

Dependencies

  • React
  • Tailwind CSS
  • motion
  • lucide-react
Implementation notes

Card exports LinkPreview with link, loading, default false, asLink, default true, and image. The row is a thumbnail, about a third of the width, plus domain, a two-line title, and a two-line description. An external-link icon fades in on hover when asLink is true. loading sets aria-busy and replaces the body with a skeleton. The screen reader text is Loading preview for {url}. The sample is a nature.com article on bumblebees along 1,200 km of Dutch roadsides, with a drawn verge as image. Without image, Thumbnail paints a gradient from the domain string.

Choosing a layout

Use the card when a reply includes a page the person might open, and you already have the title. Keep loading true until that metadata arrives, then pass the filled link. Use the hover card when the URL is inline in a paragraph and a full card would break the line. The preview uses it on a Python 3.13 what's-new page, PEP 703, and an LWN article, each with its own drawn art. Use the composer list when someone pastes a URL into the message they are about to send, and they should see whether the page was readable before they send. Use an embed when the link is a video, a repository, or a short post and a generic title card would throw away the useful fields.

Do not use a link card as a citation marker. It does not take a claim, a passage, or a list of sources. That is Inline citation and Sources list. Do not use the video embed as a real player. It advances a local timer from duration, which is a number of seconds, and it does not load media. Do not put the composer list in the answer. It is an input: remove a link, or toggle include page text, before the message is sent.

The hover card opens too slowly. Which prop is that?

openDelay, in milliseconds, default 350. It applies to both pointer enter and focus. closeDelay, default 140, is the wait before the card closes after pointer leave or blur. Moving onto the card clears that timer, so the reader can reach the link. Escape does not wait. boundary is required: the card is positioned inside that element and clamped to it, with an 8px gap. If the card seems to appear in the wrong place, the boundary ref is pointing at the wrong box.

How does a pasted link show that the page could not be read?

Set status to failed and pass error. The row turns red, the mark is grey, and the error string replaces the title. The sample uses Couldn't read this page – paywalled. The include-text switch is only rendered when status is ready, so a failed page cannot be toggled into the prompt. tokens is the short string you show next to that switch, such as ~14k for a long report. onRemove still works for loading, ready, and failed rows. The component never sets status itself.