Link tooltip that reveals a website preview card with title, description, image and favicon on hover.
Made by LéoBuilt with Next.js and styled with Tailwind CSS. Animated with Motion.
"use client";
import { TooltipPreview } from "@/components/unlumen-ui/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>
);
};Installation
Install the following dependencies:
Copy and paste the following code into your project:
"use client";
import { useState, useRef, 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 timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
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 ? (
<img
src={favicon}
alt=""
className="h-4 w-4 rounded-sm object-contain"
aria-hidden
/>
) : (
<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. |