Please use Desktop to view and interact with components
Blocks600msspring
Gallery Grid with Lightbox
Filterable image gallery with animated lightbox and navigation
galleryimageslightboxfilterportfolioshadcn
Useto navigate between components
Preview
Gallery
Our Portfolio
Explore our collection of stunning visuals and creative work
Abstract Architecture
Architecture
Modern Design
Design
Urban Landscape
Nature
Digital Art
Art
Creative Space
Architecture
Minimalist View
Design
This component requires shadcn/ui
This component uses shadcn/ui components. Make sure you have shadcn/ui set up in your project.
- Install shadcn/ui:
npx shadcn-ui@latest init - Install required components based on the imports in the code (e.g.,
npx shadcn-ui@latest add button) - Ensure your
tailwind.config.tsandglobals.cssare configured as per shadcn/ui documentation
Code
TypeScript + React
'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Card } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { X, ZoomIn, ChevronLeft, ChevronRight, Grid } from 'lucide-react'
const galleryImages = [
{
id: 1,
url: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=500',
title: 'Abstract Architecture',
category: 'Architecture',
},
{
id: 2,
url: 'https://images.unsplash.com/photo-1618556450994-a6a128ef0d9d?w=500',
title: 'Modern Design',
category: 'Design',
},
{
id: 3,
url: 'https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=500',
title: 'Urban Landscape',
category: 'Nature',
},
{
id: 4,
url: 'https://images.unsplash.com/photo-1634017839464-5c339ebe3cb4?w=500',
title: 'Digital Art',
category: 'Art',
},
{
id: 5,
url: 'https://images.unsplash.com/photo-1618556450991-2f1af64e8191?w=500',
title: 'Creative Space',
category: 'Architecture',
},
{
id: 6,
url: 'https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=500',
title: 'Minimalist View',
category: 'Design',
},
]
export function GalleryGridBlock() {
const [selectedImage, setSelectedImage] = useState<number | null>(null)
const [filter, setFilter] = useState<string>('All')
const categories = ['All', ...new Set(galleryImages.map((img) => img.category))]
const filteredImages =
filter === 'All'
? galleryImages
: galleryImages.filter((img) => img.category === filter)
const handleNext = () => {
if (selectedImage !== null) {
const currentIndex = galleryImages.findIndex((img) => img.id === selectedImage)
const nextIndex = (currentIndex + 1) % galleryImages.length
setSelectedImage(galleryImages[nextIndex].id)
}
}
const handlePrev = () => {
if (selectedImage !== null) {
const currentIndex = galleryImages.findIndex((img) => img.id === selectedImage)
const prevIndex = (currentIndex - 1 + galleryImages.length) % galleryImages.length
setSelectedImage(galleryImages[prevIndex].id)
}
}
const selectedImageData = galleryImages.find((img) => img.id === selectedImage)
return (
<section className="w-full bg-background px-4 py-16">
<div className="mx-auto max-w-7xl">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="mb-12 text-center"
>
<Badge className="mb-4" variant="secondary">
<Grid className="mr-1 h-3 w-3" />
Gallery
</Badge>
<h2 className="mb-4 text-4xl font-bold tracking-tight">
Our Portfolio
</h2>
<p className="mx-auto max-w-2xl text-muted-foreground">
Explore our collection of stunning visuals and creative work
</p>
</motion.div>
{/* Filter Buttons */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="mb-8 flex flex-wrap justify-center gap-2"
>
{categories.map((category) => (
<Button
key={category}
variant={filter === category ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter(category)}
>
{category}
</Button>
))}
</motion.div>
{/* Gallery Grid */}
<motion.div
layout
className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
>
<AnimatePresence mode="popLayout">
{filteredImages.map((image, index) => (
<motion.div
key={image.id}
layout
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
>
<Card
className="group relative cursor-pointer overflow-hidden border-border transition-all hover:border-ring hover:shadow-xl"
onClick={() => setSelectedImage(image.id)}
>
<div className="relative aspect-square overflow-hidden">
<motion.img
src={image.url}
alt={image.title}
className="h-full w-full object-cover"
whileHover={{ scale: 1.1 }}
transition={{ duration: 0.3 }}
/>
{/* Overlay */}
<motion.div
initial={{ opacity: 0 }}
whileHover={{ opacity: 1 }}
transition={{ duration: 0.2 }}
className="absolute inset-0 flex flex-col items-center justify-center bg-black/60 backdrop-blur-sm"
>
<ZoomIn className="mb-2 h-8 w-8 text-white" />
<h3 className="mb-1 text-center text-lg font-semibold text-white">
{image.title}
</h3>
<Badge variant="secondary">{image.category}</Badge>
</motion.div>
</div>
</Card>
</motion.div>
))}
</AnimatePresence>
</motion.div>
{/* Lightbox */}
<AnimatePresence>
{selectedImage !== null && selectedImageData && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 p-4"
onClick={() => setSelectedImage(null)}
>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
onClick={(e) => e.stopPropagation()}
className="relative max-h-[90vh] max-w-5xl"
>
{/* Close Button */}
<Button
size="icon"
variant="ghost"
className="absolute -right-12 top-0 text-white hover:bg-white/10"
onClick={() => setSelectedImage(null)}
>
<X className="h-6 w-6" />
</Button>
{/* Navigation Buttons */}
<Button
size="icon"
variant="ghost"
className="absolute left-4 top-1/2 -translate-y-1/2 text-white hover:bg-white/10"
onClick={(e) => {
e.stopPropagation()
handlePrev()
}}
>
<ChevronLeft className="h-8 w-8" />
</Button>
<Button
size="icon"
variant="ghost"
className="absolute right-4 top-1/2 -translate-y-1/2 text-white hover:bg-white/10"
onClick={(e) => {
e.stopPropagation()
handleNext()
}}
>
<ChevronRight className="h-8 w-8" />
</Button>
{/* Image */}
<motion.img
key={selectedImage}
src={selectedImageData.url}
alt={selectedImageData.title}
className="max-h-[80vh] w-auto rounded-lg"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
/>
{/* Image Info */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="mt-4 text-center text-white"
>
<h3 className="mb-2 text-xl font-semibold">
{selectedImageData.title}
</h3>
<Badge variant="secondary">
{selectedImageData.category}
</Badge>
</motion.div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
</section>
)
}
How to Use
- 1Install Framer Motion:
npm install framer-motion - 2Set up shadcn/ui: Install shadcn/ui components used in this code. Check the imports in the code above and install the required components (e.g.,
npx shadcn-ui@latest add button card) - 3Copy the code from above
- 4Paste it into your project and customize as needed
- 5Colors are customizable via Tailwind CSS classes. The default theme uses dark mode colors defined in your globals.css file