Gallery Showcase
GalleryShowcase is a responsive image gallery component that displays a featured image alongside animated thumbnail previews, allowing users to quickly explore and switch between images.
Installation
Install with CLI
Choose your package manager.
npx @praveenlodhi/elixir-ui add gallery-showcasepnpm dlx @praveenlodhi/elixir-ui add gallery-showcaseyarn dlx @praveenlodhi/elixir-ui add gallery-showcasebun x @praveenlodhi/elixir-ui add gallery-showcaseInstall dependencies
npm install motionpnpm add motionyarn add motionbun add motionCreate component file
Create a file at components/ui/gallery-showcase.tsx and paste the
GalleryShowcase source.
"use client";
import { useRef, useState } from "react";
import Image from "next/image";
import clsx from "clsx";
import { AnimatePresence, motion, useScroll, useTransform } from "motion/react";
import { cn } from "../lib/utils";
const DEFAULT_IMAGES = [
"https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1488590528505-98d2b5aba04b?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1516321497487-e288fb19713f?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1517430816045-df4b7de11d1d?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1470790376778-a9fbc86d70e2?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1461749280684-dccba630e2f6?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1504639725590-34d0984388bd?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=1400&q=80",
];
export interface GalleryShowcaseProps {
images?: string[];
}
export function GalleryShowcase({
images = DEFAULT_IMAGES,
}: GalleryShowcaseProps) {
const nonEmptyImages = images.filter(
(image) => typeof image === "string" && image.trim().length > 0
);
const [activeImage, setActiveImage] = useState(nonEmptyImages[0]);
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["20% end", "70% end"],
});
const leftX = useTransform(scrollYProgress, [0, 1], [-0, 0]);
const rightX = useTransform(scrollYProgress, [0, 1], [0, 0]);
const leftSkew = useTransform(scrollYProgress, [0, 1], [-6, 0]);
const rightSkew = useTransform(scrollYProgress, [0, 1], [6, 0]);
const scale = useTransform(scrollYProgress, [0, 1], [0.95, 1]);
if (nonEmptyImages.length === 0) return null;
return (
<motion.div
ref={ref}
className={cn(
clsx(
"grid",
"h-[22vh]",
"grid-cols-[4fr_0.8fr]",
"gap-1",
"md:h-[32vh]",
"md:gap-3",
"lg:h-[calc(100vh-5rem)]"
)
)}
>
{/* LEFT */}
<motion.div
style={{ x: leftX, scale, skew: rightSkew }}
className={cn(
clsx(
"relative",
"overflow-hidden",
"rounded-md",
"md:rounded-2xl",
"lg:rounded-3xl"
)
)}
>
<AnimatePresence mode="popLayout">
{activeImage && (
<motion.div
key={activeImage}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 1, y: -20 }}
transition={{ duration: 0.35, ease: "easeOut" }}
className={cn("absolute inset-0")}
>
<Image
src={activeImage}
alt={activeImage}
height={1000}
width={1000}
className={cn(
clsx("aspect-video", "h-full", "w-full", "object-cover")
)}
priority
/>
</motion.div>
)}
</AnimatePresence>
<div className={cn("absolute h-full w-full bg-black opacity-5")} />
</motion.div>
{/* RIGHT */}
<motion.div
style={{ x: rightX, scale, skew: leftSkew }}
className={cn(
clsx(
"no-scrollbar",
"flex",
"flex-col",
"gap-1",
"overflow-y-auto",
"md:gap-3"
)
)}
>
{nonEmptyImages.map((image: string) => (
<div
key={image}
onMouseEnter={() => setActiveImage(image)}
className={cn("cursor-pointer")}
>
<div
className={cn(
clsx(
"group",
"relative",
"aspect-square",
"overflow-hidden",
"rounded-md",
"md:aspect-video",
"md:rounded-xl",
"lg:rounded-2xl"
)
)}
>
<Image
src={image}
alt={image}
width={500}
height={500}
priority
className={cn(
clsx(
"h-full",
"scale-110",
"object-cover",
"transition-all",
"duration-300",
"ease-out",
"group-hover:scale-100"
)
)}
/>
<div
className={cn(
"absolute top-0 h-full w-full bg-black opacity-5"
)}
/>
</div>
</div>
))}
</motion.div>
</motion.div>
);
}Usage
import { GalleryShowcase } from "@/components/ui/gallery-showcase";
export default function DemoPage() {
return (
<GalleryShowcase
images={[
"/path/to/image-1.jpg",
"/path/to/image-2.jpg",
"/path/to/image-3.jpg",
]}
/>
);
}Props
Prop
Type
Motion Sidebar
Motion Sidebar is an animated sidebar menu that expands from a compact toggle into a full navigation panel with staggered link transitions.
Masonry Grid
Masonry Grid is a responsive layout component that arranges items in a staggered, gap-free structure similar to a Pinterest-style layout. It adapts to varying content heights while maintaining a balanced and visually appealing grid, making it ideal for galleries, cards, and dynamic content collections.