Hover Gallery
UI UX Design
Frontend
Production
Install
Update global.css
Add the following code to your global.css
file:
@theme {
--ease-spring: linear(
0,
0.009,
0.035 2.1%,
0.141,
0.281 6.7%,
0.723 12.9%,
0.938 16.7%,
1.017,
1.077,
1.121,
1.149 24.3%,
1.159,
1.163,
1.161,
1.154 29.9%,
1.129 32.8%,
1.051 39.6%,
1.017 43.1%,
0.991,
0.977 51%,
0.974 53.8%,
0.975 57.1%,
0.997 69.8%,
1.003 76.9%,
1.004 83.8%,
1
);
--ease-bounce: linear(
0,
0.004,
0.016,
0.035,
0.063,
0.098,
0.141 13.6%,
0.25,
0.391,
0.563,
0.765,
1,
0.891 40.9%,
0.848,
0.813,
0.785,
0.766,
0.754,
0.75,
0.754,
0.766,
0.785,
0.813,
0.848,
0.891 68.2%,
1 72.7%,
0.973,
0.953,
0.941,
0.938,
0.941,
0.953,
0.973,
1,
0.988,
0.984,
0.988,
1
);
}
Copy and paste the following code into your project.
"use client";
import { useCallback, useRef, useState } from "react";
export interface HoverCardProps {
cards: { name: string; left: string; top: string; right: string }[];
}
function HoverCard({ cards }: HoverCardProps) {
const ulRef = useRef<HTMLUListElement | null>(null);
const [index, setIndex] = useState(0);
const [coordinates, setCoordinates] = useState<{
top: { x: number; y: number; angle: number };
left: { x: number; y: number; angle: number };
right: { x: number; y: number; angle: number };
}>();
const handlePointerEnter = useCallback(
(e: React.PointerEvent<HTMLParagraphElement>, index: number) => {
if (!ulRef.current) return;
const ulRect = ulRef.current.getBoundingClientRect();
const rect = e.currentTarget.getBoundingClientRect();
const y = rect.top - ulRect.top;
const coordinates = {
top: {
y,
x: Math.floor(Math.random() * 61) - 30,
angle: getRandomAngle(),
},
left: {
x: rect.left - ulRect.left,
y,
angle: getRandomAngle(),
},
right: {
x: rect.right - ulRect.right,
y,
angle: getRandomAngle(),
},
};
setIndex(index);
setCoordinates(coordinates);
},
[cards]
);
return (
<div className="h-full flex items-center justify-center text-5xl">
<ul
ref={ulRef}
className="flex flex-col pointer-events-none items-center font-bold relative group"
>
{cards.map((card, index) => (
<li key={card.name}>
<p
className="cursor-pointer pointer-events-auto w-fit uppercase opacity-25 hover:opacity-100 [transition:opacity_0.15s_var(--ease-in-out),scale_0.5s_var(--ease-bounce)] hover:scale-y-110"
onPointerEnter={(e) => handlePointerEnter(e, index)}
>
{card.name}
</p>
</li>
))}
<img
src={cards[index].top}
className="absolute w-40 pointer-events-none scale-0 group-hover:scale-100 group-hover:[transition:scale_0.5s_var(--ease-spring),translate_0.5s_var(--ease-spring),rotate_1s_var(--ease-spring)] [transition:scale_0.2s_var(--ease-in-out)] rounded-md overflow-hidden shadow-xl shadow-black/25"
style={{
translate: `calc(${coordinates?.top?.x ?? 0}px) calc(-100% + ${
(coordinates?.top?.y ?? 0) - 40
}px)`,
rotate: `${coordinates?.top?.angle}deg`,
}}
alt=""
/>
<img
src={cards[index].left}
className="absolute left-0 w-40 pointer-events-none scale-0 group-hover:scale-100 group-hover:[transition:scale_0.5s_var(--ease-spring),translate_0.5s_var(--ease-spring),rotate_1s_var(--ease-spring)] [transition:scale_0.2s_var(--ease-in-out)] rounded-md overflow-hidden shadow-xl shadow-black/25"
style={{
translate: `calc(-100% + ${
coordinates?.left?.x ?? 0
}px - 100px) calc(-50% + ${coordinates?.top?.y ?? 0}px)`,
rotate: `${coordinates?.left?.angle}deg`,
}}
alt=""
/>
<img
src={cards[index].right}
className="absolute right-0 w-40 pointer-events-none scale-0 group-hover:scale-100 group-hover:[transition:scale_0.5s_var(--ease-spring),translate_0.5s_var(--ease-spring),rotate_1s_var(--ease-spring)] [transition:scale_0.2s_var(--ease-in-out)] rounded-md overflow-hidden shadow-xl shadow-black/25"
style={{
translate: `calc(100% + ${
coordinates?.right?.x ?? 0
}px + 100px) calc(-50% + ${coordinates?.top?.y ?? 0}px)`,
rotate: `${coordinates?.right?.angle}deg`,
}}
alt=""
/>
</ul>
</div>
);
}
function getRandomAngle() {
return Math.floor(Math.random() * 21) - 20;
}
export { HoverCard };
Inspired by spencergabor.