Link Preview
Inline links that show a preview card on hover or keyboard focus.
Built with Motion and Tailwind CSS. Hover a link.
components/ui/link-preview.tsx
"use client";
import { AnimatePresence, motion } from "motion/react";
import { ArrowUpRight } from "lucide-react";
import { useRef, useState, type ReactNode } from "react";
type LinkPreviewProps = {
href: string;
children: ReactNode;
title: string;
description: string;
/** Two CSS colours for the preview thumbnail (swap for a real image if you have one) */
colors?: [string, string];
};
/** Inline link that shows a preview card on hover or keyboard focus, after a short delay. */
export function LinkPreview({ href, children, title, description, colors = ["#2563eb", "#7c3aed"] }: LinkPreviewProps) {
const [open, setOpen] = useState(false);
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
const show = () => {
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => setOpen(true), 180);
};
const hide = () => {
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => setOpen(false), 120);
};
let host = href;
try {
host = new URL(href).hostname.replace(/^www\./, "");
} catch {}
return (
<span className="relative inline-block" onMouseEnter={show} onMouseLeave={hide}>
<a href={href} target="_blank" rel="noreferrer" onFocus={show} onBlur={hide} className="font-medium text-zinc-900 underline decoration-zinc-300 underline-offset-4 transition-colors hover:decoration-zinc-900">
{children}
</a>
<AnimatePresence>
{open && (
<motion.span
role="tooltip"
initial={{ opacity: 0, y: 8, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 4, scale: 0.98 }}
transition={{ type: "spring", stiffness: 380, damping: 28 }}
className="absolute bottom-full left-1/2 z-30 mb-3 block w-64 origin-bottom -translate-x-1/2 overflow-hidden rounded-2xl border border-zinc-200 bg-white text-left shadow-2xl"
>
<span className="relative block h-28" style={{ background: `linear-gradient(135deg, ${colors[0]}, ${colors[1]})` }}>
<span className="absolute inset-0 opacity-30 [background-image:radial-gradient(#fff_1px,transparent_1px)] [background-size:12px_12px]" />
<span className="absolute bottom-3 left-3 rounded-md bg-white/90 px-2 py-0.5 text-[11px] font-medium text-zinc-800">{host}</span>
</span>
<span className="block p-3.5">
<span className="flex items-center justify-between gap-2 text-sm font-semibold text-zinc-900">
{title} <ArrowUpRight className="size-3.5 shrink-0 text-zinc-400" />
</span>
<span className="mt-1 block text-xs leading-relaxed text-zinc-500">{description}</span>
</span>
</motion.span>
)}
</AnimatePresence>
</span>
);
}
export default function Demo() {
return (
<p className="max-w-md pt-40 text-center text-lg leading-relaxed text-zinc-600">
Built with{" "}
<LinkPreview href="https://motion.dev" title="Motion" description="A production-grade animation library for React, JavaScript and Vue.">
Motion
</LinkPreview>{" "}
and{" "}
<LinkPreview href="https://tailwindcss.com" title="Tailwind CSS" description="A utility-first CSS framework for building designs right in your markup." colors={["#06b6d4", "#3b82f6"]}>
Tailwind CSS
</LinkPreview>
. Hover a link.
</p>
);
}
Edit with AIPro
Describe a change to Link Preview and get the updated code. The live preview above stays as the original.
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/link-preview.tsxcomponents/ui/link-preview.tsx"use client"; import { AnimatePresence, motion } from "motion/react"; import { ArrowUpRight } from "lucide-react"; import { useRef, useState, type ReactNode } from "react"; type LinkPreviewProps = { href: string; children: ReactNode; title: string; description: string; /** Two CSS colours for the preview thumbnail (swap for a real image if you have one) */ colors?: [string, string]; }; /** Inline link that shows a preview card on hover or keyboard focus, after a short delay. */ export function LinkPreview({ href, children, title, description, colors = ["#2563eb", "#7c3aed"] }: LinkPreviewProps) { const [open, setOpen] = useState(false); const timer = useRef<ReturnType<typeof setTimeout> | null>(null); const show = () => { if (timer.current) clearTimeout(timer.current); timer.current = setTimeout(() => setOpen(true), 180); }; const hide = () => { if (timer.current) clearTimeout(timer.current); timer.current = setTimeout(() => setOpen(false), 120); }; let host = href; try { host = new URL(href).hostname.replace(/^www\./, ""); } catch {} return ( <span className="relative inline-block" onMouseEnter={show} onMouseLeave={hide}> <a href={href} target="_blank" rel="noreferrer" onFocus={show} onBlur={hide} className="font-medium text-zinc-900 underline decoration-zinc-300 underline-offset-4 transition-colors hover:decoration-zinc-900"> {children} </a> <AnimatePresence> {open && ( <motion.span role="tooltip" initial={{ opacity: 0, y: 8, scale: 0.96 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 4, scale: 0.98 }} transition={{ type: "spring", stiffness: 380, damping: 28 }} className="absolute bottom-full left-1/2 z-30 mb-3 block w-64 origin-bottom -translate-x-1/2 overflow-hidden rounded-2xl border border-zinc-200 bg-white text-left shadow-2xl" > <span className="relative block h-28" style={{ background: `linear-gradient(135deg, ${colors[0]}, ${colors[1]})` }}> <span className="absolute inset-0 opacity-30 [background-image:radial-gradient(#fff_1px,transparent_1px)] [background-size:12px_12px]" /> <span className="absolute bottom-3 left-3 rounded-md bg-white/90 px-2 py-0.5 text-[11px] font-medium text-zinc-800">{host}</span> </span> <span className="block p-3.5"> <span className="flex items-center justify-between gap-2 text-sm font-semibold text-zinc-900"> {title} <ArrowUpRight className="size-3.5 shrink-0 text-zinc-400" /> </span> <span className="mt-1 block text-xs leading-relaxed text-zinc-500">{description}</span> </span> </motion.span> )} </AnimatePresence> </span> ); } export default function Demo() { return ( <p className="max-w-md pt-40 text-center text-lg leading-relaxed text-zinc-600"> Built with{" "} <LinkPreview href="https://motion.dev" title="Motion" description="A production-grade animation library for React, JavaScript and Vue."> Motion </LinkPreview>{" "} and{" "} <LinkPreview href="https://tailwindcss.com" title="Tailwind CSS" description="A utility-first CSS framework for building designs right in your markup." colors={["#06b6d4", "#3b82f6"]}> Tailwind CSS </LinkPreview> . Hover a link. </p> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.