All components
Link Preview
tooltipsAceternity UI component.
responsive · 460px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/link-preview.jsonUsage
"use client";
import { LinkPreview } from "@/registry/aceternity-ui/link-preview";
export default function Demo() {
return (
<div className="flex min-h-[320px] flex-col items-center justify-center gap-6 p-12 text-center">
<p className="text-sm text-muted-foreground">Hover over the links to see a preview</p>
<p className="text-lg font-medium text-foreground">
Built with{" "}
<LinkPreview
url="https://nextjs.org"
isStatic
imageSrc="https://assets.vercel.com/image/upload/v1662130559/nextjs/og.png"
width={200}
height={125}
className="font-bold text-foreground underline underline-offset-4"
>
Next.js
</LinkPreview>{" "}
and deployed on{" "}
<LinkPreview
url="https://vercel.com"
isStatic
imageSrc="https://assets.vercel.com/image/upload/v1588805858/repositories/vercel/logo.png"
width={200}
height={125}
className="font-bold text-foreground underline underline-offset-4"
>
Vercel
</LinkPreview>
.
</p>
</div>
);
}Component source
"use client";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { encode } from "qss";
import React from "react";
import {
AnimatePresence,
motion,
useMotionValue,
useSpring,
} from "motion/react";
import { cn } from "@/lib/utils";
type LinkPreviewProps = {
children: React.ReactNode;
url: string;
className?: string;
width?: number;
height?: number;
quality?: number;
layout?: string;
} & (
| { isStatic: true; imageSrc: string }
| { isStatic?: false; imageSrc?: never }
);
export const LinkPreview = ({
children,
url,
className,
width = 200,
height = 125,
quality = 50,
layout = "fixed",
isStatic = false,
imageSrc = "",
}: LinkPreviewProps) => {
let src;
if (!isStatic) {
const params = encode({
url,
screenshot: true,
meta: false,
embed: "screenshot.url",
colorScheme: "dark",
"viewport.isMobile": true,
"viewport.deviceScaleFactor": 1,
"viewport.width": width * 3,
"viewport.height": height * 3,
});
src = `https://api.microlink.io/?${params}`;
} else {
src = imageSrc;
}
const [isOpen, setOpen] = React.useState(false);
const [isMounted, setIsMounted] = React.useState(false);
React.useEffect(() => {
setIsMounted(true);
}, []);
const springConfig = { stiffness: 100, damping: 15 };
const x = useMotionValue(0);
const translateX = useSpring(x, springConfig);
const handleMouseMove = (event: any) => {
const targetRect = event.target.getBoundingClientRect();
const eventOffsetX = event.clientX - targetRect.left;
const offsetFromCenter = (eventOffsetX - targetRect.width / 2) / 2; // Reduce the effect to make it subtle
x.set(offsetFromCenter);
};
return (
<>
{isMounted ? (
<div className="hidden">
<img
src={src}
width={width}
height={height}
alt="hidden image"
/>
</div>
) : null}
<HoverCardPrimitive.Root
openDelay={50}
closeDelay={100}
onOpenChange={(open) => {
setOpen(open);
}}
>
<HoverCardPrimitive.Trigger
onMouseMove={handleMouseMove}
className={cn("text-black dark:text-white", className)}
href={url}
>
{children}
</HoverCardPrimitive.Trigger>
<HoverCardPrimitive.Content
className="[transform-origin:var(--radix-hover-card-content-transform-origin)]"
side="top"
align="center"
sideOffset={10}
>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.6 }}
animate={{
opacity: 1,
y: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 260,
damping: 20,
},
}}
exit={{ opacity: 0, y: 20, scale: 0.6 }}
className="shadow-xl rounded-xl"
style={{
x: translateX,
}}
>
<a
href={url}
className="block p-1 bg-white border-2 border-transparent shadow rounded-xl hover:border-neutral-200 dark:hover:border-neutral-800"
style={{ fontSize: 0 }}
>
<img
src={isStatic ? imageSrc : src}
width={width}
height={height}
className="rounded-lg"
alt="preview image"
/>
</a>
</motion.div>
)}
</AnimatePresence>
</HoverCardPrimitive.Content>
</HoverCardPrimitive.Root>
</>
);
};Dependencies
@radix-ui/react-hover-cardqssmotion
Source: Aceternity UI