A React link that shows a live website preview on hover. Built with Motion and Tailwind CSS for docs, blogs and portfolios.
<LinkPreview url={"https://www.google.com"}>
<Link
className="font-medium font-serif text-xl"
href="https://www.google.com"
target="_blank"
>
Google
</Link>
</LinkPreview>npm install motion radix-uibun add motion radix-uideno add motion radix-uipnpm add motion radix-uiyarn add motion radix-uiCopy and paste the following code into your project.
"use client";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import {
AnimatePresence,
motion,
useMotionValue,
useReducedMotion,
useSpring,
} from "motion/react";
import Image from "next/image";
import Link from "next/link";
import React from "react";
interface LinkPreviewProps {
children: React.ReactNode;
className?: string;
height?: number;
url: string;
width?: number;
}
const MotionLink = motion.create(Link);
export const LinkPreview = ({
children,
url,
className,
width = 200,
height = 125,
}: LinkPreviewProps) => {
const params = new URLSearchParams({
colorScheme: "dark",
embed: "screenshot.url",
meta: "false",
screenshot: "true",
url,
"viewport.deviceScaleFactor": "1",
"viewport.height": String(height * 3),
"viewport.isMobile": "true",
"viewport.width": String(width * 3),
});
const src = `https://api.microlink.io/?${params}`;
const [isOpen, setOpen] = React.useState(false);
const shouldReduceMotion = useReducedMotion();
const springConfig = shouldReduceMotion
? undefined
: { damping: 15, stiffness: 100 };
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;
x.set(offsetFromCenter);
};
return (
<HoverCardPrimitive.Root
closeDelay={100}
onOpenChange={(open) => {
setOpen(open);
}}
openDelay={50}
>
<HoverCardPrimitive.Trigger onMouseMove={handleMouseMove}>
{children}
</HoverCardPrimitive.Trigger>
<HoverCardPrimitive.Content
align="center"
aria-label={`Preview of ${url}`}
className="origin-(--radix-hover-card-content-transform-origin)"
side="top"
sideOffset={10}
>
<AnimatePresence>
{isOpen ? (
<MotionLink
animate={{
opacity: 1,
scale: 1,
transition: shouldReduceMotion
? undefined
: {
damping: 20,
stiffness: 260,
type: "spring",
},
y: 0,
}}
aria-label={`Open ${url} in a new window`}
className={`block rounded-xl border-2 border-[#d8d8d8c9] bg-white p-1 shadow-lg focus:outline-none focus:ring-2 focus:ring-[#d8d8d8c9] focus:ring-offset-2 ${className}`}
exit={{ opacity: 0, scale: 0.6, y: 20 }}
href={url}
initial={{ opacity: 0, scale: 0.6, y: 20 }}
rel="noopener noreferrer"
style={{
x: translateX,
}}
target="_blank"
>
<Image
alt={`Preview of ${url}`}
className="rounded-lg"
height={height}
priority={true}
src={src}
width={width}
/>
</MotionLink>
) : null}
</AnimatePresence>
</HoverCardPrimitive.Content>
</HoverCardPrimitive.Root>
);
};<LinkPreview url="https://www.google.com">
<Link href="https://www.google.com">Google</Link>
</LinkPreview><LinkPreview url="https://www.google.com" className="rounded-none border-black shadow-none">
<Link href="https://www.google.com">Google</Link>
</LinkPreview><LinkPreview url="https://www.google.com">
<div className="custom-google-card">
<h2>Google</h2>
<p>Google is a search engine etc etc</p>
</div>
</LinkPreview>| Prop | Type | Default | Description |
|---|---|---|---|
| url | string | - | The URL of the link to preview |
| className | string | - | The class name of the link preview |
| height | number | 125 | The height of the link preview |
| width | number | 200 | The width of the link preview |
| children | React.ReactNode | - | The content of the link preview |