Link unfurlPro
A composer where pasting a URL shows a loading shimmer on the link, then unfurls it into a rich preview card with title, image and favicon in one morph, which can be collapsed back to the inline link or removed.
Paste a link to preview it. Alt and Enter shows or collapses the preview of the link before the caret. Enter sends, Shift and Enter adds a line.
Demo composer, nothing is sent. Paste any link, click a link to fold its card, or drag a card up.
- A message or comment composer where pasted links should show what they point to before sending.
- Product marketing that shows how a chat or notes app handles links.
- Use hover-card to preview a link someone reads, not writes.
- Use mention-input when the tokens are people or records rather than web links, and rich-text-editor for long formatted documents.
Installation
Pro source and install commands unlock with a Pro plan.
import { LinkUnfurl, type LinkUnfurlPreview } from "@/registry/components/link-unfurl/link-unfurl"; export function ChannelComposer({ channel, post }: { channel: string; post: (text: string) => Promise<void> }) { return <LinkUnfurl label={`Message #${channel}`} placeholder={`Message #${channel}`} defaultValue="" samples={[]} autoPlay={false} resolve={async (url, signal) => { const res = await fetch(`/api/unfurl?url=${encodeURIComponent(url)}`, { signal }); return res.ok ? (await res.json()) as LinkUnfurlPreview : null; }} onSend={message => post(message.text)} />;}LinkUnfurl
A message composer that unfurls links. Paste an address and it becomes an inline link with a soft loading sweep; once the preview resolves, the favicon and title roll in and the link unrolls into a rich card with site, title, description, and image in one continuous morph. The card's corner grows out of the link itself, width first and height after, lifting on a shadow while it travels. Click the link, press the collapse button, or drag the card up to fold it back into the text; remove it to take the link out of the message.
labelstring"Message #lisbon-offsite"Accessible name of the message field.placeholderstring"Message #lisbon-offsite"Shown while the message is empty.defaultValuestring"Found a place for the offsite, take a look"Starting text of the message.samplesLinkUnfurlPreview[]–Sample links offered as buttons under the field; the built in resolver answers with their previews. Once a sample is in the message its button shows or collapses its card. Pass an empty array to hide them.resolve(url: string, signal: AbortSignal) => Promise<LinkUnfurlPreview | null>–Fetches the preview for a pasted address, usually from your own unfurl endpoint. Return null when there is none; the link then stays in the text without a card and can be clicked to try again. Without it, samples resolve by address and other links get a plain preview built from the address after a 1.15s simulated fetch.autoExpandbooleantrueUnfurls a card as soon as its preview resolves. When false the link only gains its favicon and title, and opens on click.autoPlaybooleantruePastes the first sample the first time the composer scrolls into view, unless someone has already interacted.speednumber1Multiplier for the morph and the simulated fetch.pausedbooleanfalseOpens and folds cards instantly without the morph and turns autoplay off.accentstring–Color of an unfurled link and its tie to its card. Defaults to the accent.sendLabelstring"Send message"Accessible name of the send button.onSend(message: LinkUnfurlMessage) => void | Promise<unknown>–Called on Enter or the send button with the text (links written out as addresses) and the resolved previews. Return a promise to hold the composer while it is pending; a rejection shows its message under the composer. On success the composer clears and the button confirms with a check.classNamestring–Extra class on the root element.styleCSSProperties–Inline styles on the root element.- The message is a contenteditable element with role="textbox", aria-multiline, a label, and a description of its keys. Pasting inserts plain text, and a pasted or typed address (followed by a space) becomes an atomic link that deletes as one unit.
- Keyboard: Enter sends, Shift+Enter adds a line, and Alt+Enter shows or collapses the preview of the link before the caret. Every card has a named collapse button and a named remove button, and its title is a real link.
- A folded card is inert and hidden from assistive technology, so only open previews are in the tab order.
- A polite live region announces a link being added and loading, the preview opening with its site and title, collapsing, removal, a failed preview, and the sent message. Send errors use role="alert".
- Loading is shown by the sweep and by muted text and a globe instead of a favicon, and announced, never by color alone.
- No focus ring is drawn; focused controls take a muted fill.
- One rAF loop drives every card from a critically damped spring, writing only transforms, clip paths, opacity, and the plate size; React renders once per state change, never per frame.
- The card is clipped to the exact rectangle of its inline link, with the favicons aligned, and translated onto it. As the spring runs the clip widens first and deepens after, while the translation eases home, so the link reads as unrolling into the card. A plate under the card carries its border and a shadow that lifts only mid flight.
- The lane grows with the card's height on the same progress, so the toolbar below glides instead of jumping. The body fades in after the header, and the image settles from a slight zoom.
- When a preview resolves, the link's width morphs to fit the title while the title rolls up and the favicon pops in.
- Collapsing runs the same path backwards on a quicker spring. Dragging a card up folds it with the pointer; releasing past about 40% or with an upward flick folds it, otherwise it springs back.
- The loop sleeps once cards settle, and cards land instantly when the composer is offscreen or the tab is hidden; the loading sweep pauses with them. Reduced motion and paused open and fold cards instantly and show a still tint instead of the sweep.
- The composer sizes itself from its own width with container queries: padding steps up from 560px, and the Try label hides below 360px so the sample buttons and send button stay on one row.
- Cards are at most 440px wide and fill narrower composers; images keep the 1.91 : 1 Open Graph ratio. Links truncate at 16em with the full title in the card and the accessible name.
- On touch only the card header drags, so the page still scrolls over the card body and image.
- Each frame reads all geometry first and then writes, so the loop costs one layout pass. Nothing is created or removed while animating.
- Preview images are decoded before the card opens, so the morph never reveals an empty frame.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Use it in chat, comments, and notes where links are shared and a preview helps people decide whether to open them.
- Unfurl on your server (Open Graph and oEmbed) and return site, title, description, image, and favicon through resolve. Honor the abort signal; it fires when the link is removed or the message is sent.
- The favicon slot in the link and the card are aligned on purpose; keep favicons square.
- Brand marks in the samples are sample content, not endorsements.
The full library index for assistants is at /llms.txt.