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
Run this command in your Tailwind CSS project. It adds
components/ai/link-preview-default.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/link-preview-default.json
Usage & API
Example
import { LinkPreview } from "@/components/ai/link-preview-default"
<LinkPreview
link={link}
/>Props LinkPreview
| Prop | Type & description | Required |
|---|---|---|
link | LinkMeta | Required |
loading | boolean | Optional |
asLink | booleanRender as a plain card instead of a link, e.g. when it already sits inside one. | Optional |
image | ReactNodeThe 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.
Example
import { LinkHoverCard } from "@/components/ai/link-preview-hover"
<LinkHoverCard
link={link}
children={children}
boundary={null}
/>Props LinkHoverCard
| Prop | Type & description | Required |
|---|---|---|
link | HoverLink | Required |
children | ReactNode | Required |
boundary | RefObject<HTMLElement | null>The element the card is positioned and clamped within. | Required |
openDelay | number | Optional |
closeDelay | number | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Hover card exports LinkHoverCard with link, children, boundary, openDelay, default 350, and closeDelay, default 140. HoverLink adds a letter mark, an art kind of docs, pep, or article, and an optional meta line. children become the anchor text. The card waits openDelay after pointer enter or focus, then opens, and waits closeDelay before it closes. Escape closes it immediately. It is positioned against boundary, 300px wide unless the boundary is narrower, and it flips above the link when there is no room below. The card shows the drawn art, the domain, the title, the description, and meta. The Python sample's meta reads Python 3.13.0 documentation, and the PEP card's meta reads Accepted, created 9 Jan 2023.
Example
import { ComposerLinks } from "@/components/ai/link-preview-composer"
<ComposerLinks
links={[]}
onRemove={() => {}}
onToggleText={() => {}}
/>Props ComposerLinks
| Prop | Type & description | Required |
|---|---|---|
links | PastedLink[] | Required |
onRemove | (id: string) => void | Required |
onToggleText | (id: string) => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Pasted in composer exports ComposerLinks with links, onRemove, and onToggleText. A PastedLink has id, url, status of loading, ready, or failed, domain, an optional title, an optional error, an optional tokens string, includeText, and a mark. A loading row is a shimmer and the text Reading {url}. A failed row shows error, in the sample Couldn't read this page – paywalled for ft.com, and greys the mark. A ready row shows the title, the domain, and a switch, Include page text, with the tokens count, for example ~2.1k. The switch calls onToggleText. Every row has a remove button. The list does not parse the paste itself. The preview does that around it.
Example
import { VideoEmbed } from "@/components/ai/link-preview-embeds"
<VideoEmbed
title="…"
channel="…"
site="…"
duration={1}
/>Props VideoEmbed
| Prop | Type & description | Required |
|---|---|---|
title | string | Required |
channel | string | Required |
site | string | Required |
duration | numberLength in seconds. | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Rich embeds exports three components. VideoEmbed takes title, channel, site, and duration in seconds. Play and pause toggle a local clock, a red bar, and the label Play or Pause plus the title. It does not attach a media element. RepoCard takes owner, name, description, language with a name and a colour, stars, forks, and updated. The name links to the repository path. Star toggles aria-pressed and adds one to the displayed star count while it is on. PostCard takes author, handle, posted, text, replies, reposts, likes, and an avatar colour. A heart button switches between Like and Unlike. The three cards are separate. There is no type field that switches among them.
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.