Link tooltip that reveals a website preview card with title, description, image and favicon on hover.
Made by LéoLoading...
"use client";
import { TooltipPreview } from "@/components/unlumen-ui/primitives/tooltip-preview";
export const TooltipPreviewDemo = () => {
return (
<div className="flex flex-col items-center justify-center gap-6 p-12 text-sm text-muted-foreground leading-relaxed max-w-sm text-center">
<p>
Built with{" "}
<TooltipPreview
href="https://nextjs.org"
title="Next.js"
description="The React framework for the web. Used by the largest companies, Next.js enables you to create high-quality web applications."
image="https://nextjs.org/static/twitter-cards/home.jpg"
favicon="https://nextjs.org/favicon.ico"
>
Next.js
</TooltipPreview>{" "}
and styled with{" "}
<TooltipPreview
href="https://tailwindcss.com"
title="Tailwind CSS"
description="A utility-first CSS framework packed with classes that can be composed to build any design, directly in your markup."
image="https://tailwindcss.com/opengraph-image.jpg"
favicon="https://tailwindcss.com/favicons/favicon.ico"
>
Tailwind CSS
</TooltipPreview>
{". "}Animated with{" "}
<TooltipPreview
href="https://motion.dev"
title="Motion"
description="Motion is the only animation library with a hybrid engine, combining the power of JavaScript animations with the performance of native browser APIs."
favicon="https://motion.dev/favicon.ico"
>
Motion
</TooltipPreview>
.
</p>
</div>
);
};
// "use client";
// import { TooltipPreview } from "@/components/unlumen-ui/primitives/tooltip-preview";
// import Image from "next/image";
// export const TooltipPreviewDemo = () => {
// const nextJs = "https://nextjs.org";
// const tailwind = "https://tailwindcss.com";
// const vercel = "https://vercel.com";
// const getFavicon = (url: string) =>
// `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=128`;
// console.log(getFavicon(nextJs), getFavicon(tailwind), getFavicon(vercel));
// return (
// <div className="flex max-w-sm flex-col items-center justify-center gap-6 p-12 text-center text-sm leading-relaxed text-muted-foreground">
// <p>
// Built with{" "}
// <TooltipPreview
// href={nextJs}
// title="Next.js"
// description="The React framework for the web. Used by the largest companies, Next.js enables you to create high-quality web applications."
// image="https://nextjs.org/static/twitter-cards/home.jpg"
// favicon={getFavicon(nextJs)}
// >
// Next.js
// </TooltipPreview>{" "}
// and styled with{" "}
// <TooltipPreview
// href={tailwind}
// title="Tailwind CSS"
// description="A utility-first CSS framework packed with classes that can be composed to build any design, directly in your markup."
// image="https://tailwindcss.com/opengraph-image.jpg"
// favicon={getFavicon(tailwind)}
// >
// Tailwind CSS
// </TooltipPreview>
// {". "}Animated with{" "}
// <TooltipPreview
// href={vercel}
// title="Vercel"
// description="Vercel is a cloud hosting platform that helps developers build, deploy, and scale fast websites and web apps."
// favicon={getFavicon(vercel)}
// >
// Vercel
// </TooltipPreview>
// .
// </p>
// </div>
// );
// };Installation
Install the following dependencies:
Copy and paste the following code into your project:
"use client";
import {
useEffect,
useRef,
useState,
type ReactNode,
type HTMLAttributes,
} from "react";
import { AnimatePresence, motion } from "motion/react";
import { cn } from "@/lib/utils";
interface TooltipPreviewProps extends HTMLAttributes<HTMLAnchorElement> {
href: string;
title: string;
description?: string;
image?: string;
favicon?: string;
children: ReactNode;
}
function TooltipPreview({
href,
title,
description,
image,
favicon,
children,
className,
...props
}: TooltipPreviewProps) {
const [open, setOpen] = useState(false);
const [faviconFailed, setFaviconFailed] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
setFaviconFailed(false);
}, [favicon]);
const show = () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setOpen(true), 200);
};
const hide = () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setOpen(false), 100);
};
const domain = (() => {
try {
return new URL(href).hostname.replace(/^www\./, "");
} catch {
return href;
}
})();
return (
<span className="relative inline-block">
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={cn(
"underline decoration-dotted underline-offset-2 decoration-muted-foreground/50 hover:decoration-foreground/60 transition-colors",
className,
)}
onMouseEnter={show}
onMouseLeave={hide}
onFocus={show}
onBlur={hide}
{...props}
>
{children}
</a>
<AnimatePresence>
{open && (
<motion.div
role="tooltip"
className={cn(
"absolute bottom-full left-1/2 z-50 mb-3 w-64 -translate-x-1/2",
"rounded-xl border border-border bg-card shadow-xl shadow-black/10",
"pointer-events-none overflow-hidden",
)}
initial={{ opacity: 0, y: 6, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{
opacity: 0,
y: 4,
scale: 0.97,
transition: { duration: 0.1 },
}}
transition={{ type: "spring", duration: 0.25, bounce: 0.1 }}
onMouseEnter={show}
onMouseLeave={hide}
>
{image && (
<div className="h-32 w-full overflow-hidden border-b border-border">
<img
src={image}
alt={title}
className="h-full w-full object-cover"
/>
</div>
)}
<div className="p-3 space-y-1">
<div className="flex items-center gap-2">
{favicon && !faviconFailed ? (
<img
src={favicon}
alt=""
className="h-4 w-4 rounded-sm object-contain"
aria-hidden
onError={() => setFaviconFailed(true)}
/>
) : (
<span
className="flex h-4 w-4 items-center justify-center rounded-sm bg-muted text-[9px] font-bold text-muted-foreground uppercase shrink-0"
aria-hidden
>
{domain.charAt(0)}
</span>
)}
<span className="text-[12px] font-semibold text-foreground line-clamp-1 leading-tight">
{title}
</span>
</div>
{description && (
<p className="text-[11px] text-muted-foreground line-clamp-2 leading-relaxed">
{description}
</p>
)}
<p className="text-[11px] text-muted-foreground/60 truncate">
{domain}
</p>
</div>
</motion.div>
)}
</AnimatePresence>
</span>
);
}
export { TooltipPreview };
export type { TooltipPreviewProps };Update the import paths to match your project setup.
Usage
import { TooltipPreview } from "@/components/unlumen-ui/tooltip-preview";<TooltipPreview
href="https://nextjs.org"
title="Next.js"
description="The React framework for the web."
image="https://nextjs.org/static/twitter-cards/home.jpg"
favicon="https://nextjs.org/favicon.ico"
>
Next.js
</TooltipPreview>Props
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | The URL the link points to. |
title | string | — | Site title shown in the preview card. |
description | string | — | Optional description shown below the title. |
image | string | — | Optional OG image URL shown at the top of the card. |
favicon | string | — | Optional favicon URL. Falls back to first letter. |
children | ReactNode | — | The link text content. |
className | string | — | Extra classes on the anchor element. |
Built by Léo
Last updated: 5/14/2026