Responsive Image Gallery with CSS Grid and JavaScript Lightbox
• Updated 6/6/2026 • CSS GridResponsive Image GalleryLightboxJavaScriptHTMLCSSResponsive DesignFrontend Development
One-file front-end editor
Write plain HTML, CSS, JavaScript, or CDN-powered Vue and React demos.
What This Snippet Does
This snippet shows how to build a responsive image gallery using CSS Grid and a lightweight vanilla JavaScript lightbox. The gallery automatically adapts to different screen sizes, while the lightbox lets users preview larger images without leaving the page.
It is useful for portfolios, product galleries, travel blogs, photography websites, case studies, landing pages, and admin dashboards that need clean image previews.
Real-World Use Case
Imagine a portfolio page where each project has a thumbnail image. Users can scan the grid quickly, click an image to view it larger, use arrow keys to move between images, and press Escape to close the preview. This creates a smoother browsing experience without requiring a heavy gallery library.
Concept Overview
The gallery uses CSS Grid to create a flexible layout and vanilla JavaScript to control the lightbox. Each image button stores its full-size image URL and caption using data attributes, making the component easy to reuse and update.
Key Features
- Responsive CSS Grid layout.
- Clickable image thumbnails.
- Reusable JavaScript lightbox logic.
- Previous and next image navigation.
- Keyboard support for
Escape,ArrowLeft, andArrowRight. - Image captions for better context.
- No external dependencies or frameworks.
Complete Snippet Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Image Gallery with Lightbox</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, sans-serif;
background: #f8fafc;
color: #1f2937;
line-height: 1.5;
padding: 2rem;
}
.page-header {
max-width: 900px;
margin: 0 auto 2rem;
text-align: center;
}
.page-header h1 {
margin-bottom: 0.5rem;
font-size: clamp(2rem, 5vw, 3.5rem);
}
.page-header p {
margin: 0;
color: #64748b;
}
.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1rem;
max-width: 1100px;
margin: 0 auto;
}
.gallery-item {
display: block;
width: 100%;
border: 0;
border-radius: 14px;
padding: 0;
overflow: hidden;
cursor: pointer;
background: #ffffff;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.gallery-item:hover {
transform: translateY(-4px);
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
}
.gallery-item:focus-visible,
.lightbox-button:focus-visible,
.lightbox-close:focus-visible {
outline: 3px solid #93c5fd;
outline-offset: 4px;
}
.gallery-item img {
display: block;
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}
.gallery-caption {
display: block;
padding: 0.85rem 1rem;
font-weight: 700;
text-align: left;
}
.lightbox {
position: fixed;
inset: 0;
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
padding: 1rem;
background: rgba(15, 23, 42, 0.88);
}
.lightbox.is-open {
display: flex;
}
.lightbox-content {
position: relative;
width: min(100%, 960px);
text-align: center;
}
.lightbox-image {
max-width: 100%;
max-height: 78vh;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
}
.lightbox-caption {
margin: 1rem 0 0;
color: #ffffff;
font-size: 1rem;
}
.lightbox-close,
.lightbox-button {
border: 0;
cursor: pointer;
color: #ffffff;
background: rgba(255, 255, 255, 0.16);
backdrop-filter: blur(8px);
}
.lightbox-close {
position: absolute;
top: -3rem;
right: 0;
width: 2.5rem;
height: 2.5rem;
border-radius: 999px;
font-size: 1.5rem;
line-height: 1;
}
.lightbox-button {
position: absolute;
top: 50%;
width: 2.75rem;
height: 2.75rem;
border-radius: 999px;
transform: translateY(-50%);
font-size: 2rem;
line-height: 1;
}
.lightbox-button:hover,
.lightbox-close:hover {
background: rgba(255, 255, 255, 0.28);
}
.lightbox-prev {
left: 1rem;
}
.lightbox-next {
right: 1rem;
}
@media (max-width: 640px) {
body {
padding: 1rem;
}
.lightbox-button {
top: auto;
bottom: -4rem;
transform: none;
}
.lightbox-prev {
left: calc(50% - 4rem);
}
.lightbox-next {
right: calc(50% - 4rem);
}
}
</style>
</head>
<body>
<header class="page-header">
<h1>Project Gallery</h1>
<p>Click any image to open the responsive lightbox preview.</p>
</header>
<main>
<section class="gallery" aria-label="Project image gallery">
<button class="gallery-item" type="button" data-full="https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=1200&q=80" data-caption="Modern workspace dashboard design">
<img src="https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=600&q=80" alt="Modern workspace dashboard design">
<span class="gallery-caption">Workspace Dashboard</span>
</button>
<button class="gallery-item" type="button" data-full="https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=1200&q=80" data-caption="Laptop showing a responsive website layout">
<img src="https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=600&q=80" alt="Laptop showing a responsive website layout">
<span class="gallery-caption">Responsive Website</span>
</button>
<button class="gallery-item" type="button" data-full="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1200&q=80" data-caption="Clean landing page visual inspiration">
<img src="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=600&q=80" alt="Clean landing page visual inspiration">
<span class="gallery-caption">Landing Page Visual</span>
</button>
<button class="gallery-item" type="button" data-full="https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&w=1200&q=80" data-caption="Developer coding setup with laptop">
<img src="https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&w=600&q=80" alt="Developer coding setup with laptop">
<span class="gallery-caption">Developer Setup</span>
</button>
</section>
</main>
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Image preview" aria-hidden="true">
<div class="lightbox-content">
<button class="lightbox-close" type="button" aria-label="Close image preview">×</button>
<button class="lightbox-button lightbox-prev" type="button" aria-label="Previous image">‹</button>
<img class="lightbox-image" src="" alt="">
<p class="lightbox-caption"></p>
<button class="lightbox-button lightbox-next" type="button" aria-label="Next image">›</button>
</div>
</div>
<script>
const galleryItems = Array.from(document.querySelectorAll('.gallery-item'));
const lightbox = document.getElementById('lightbox');
const lightboxImage = lightbox.querySelector('.lightbox-image');
const lightboxCaption = lightbox.querySelector('.lightbox-caption');
const closeButton = lightbox.querySelector('.lightbox-close');
const previousButton = lightbox.querySelector('.lightbox-prev');
const nextButton = lightbox.querySelector('.lightbox-next');
let currentIndex = 0;
let previouslyFocusedElement = null;
function showImage(index) {
const item = galleryItems[index];
const fullImage = item.dataset.full;
const caption = item.dataset.caption;
const thumbnail = item.querySelector('img');
lightboxImage.src = fullImage;
lightboxImage.alt = thumbnail.alt;
lightboxCaption.textContent = caption;
currentIndex = index;
}
function openLightbox(index) {
previouslyFocusedElement = document.activeElement;
showImage(index);
lightbox.classList.add('is-open');
lightbox.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
closeButton.focus();
document.addEventListener('keydown', handleKeydown);
}
function closeLightbox() {
lightbox.classList.remove('is-open');
lightbox.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
document.removeEventListener('keydown', handleKeydown);
lightboxImage.src = '';
if (previouslyFocusedElement) {
previouslyFocusedElement.focus();
}
}
function showPreviousImage() {
const previousIndex = (currentIndex - 1 + galleryItems.length) % galleryItems.length;
showImage(previousIndex);
}
function showNextImage() {
const nextIndex = (currentIndex + 1) % galleryItems.length;
showImage(nextIndex);
}
function handleKeydown(event) {
if (event.key === 'Escape') {
closeLightbox();
}
if (event.key === 'ArrowLeft') {
showPreviousImage();
}
if (event.key === 'ArrowRight') {
showNextImage();
}
}
galleryItems.forEach((item, index) => {
item.addEventListener('click', () => openLightbox(index));
});
closeButton.addEventListener('click', closeLightbox);
previousButton.addEventListener('click', showPreviousImage);
nextButton.addEventListener('click', showNextImage);
lightbox.addEventListener('click', (event) => {
if (event.target === lightbox) {
closeLightbox();
}
});
</script>
</body>
</html>
How It Works
- The
.gallerycontainer usesdisplay: gridandrepeat(auto-fit, minmax(220px, 1fr))to create a responsive layout. - Each gallery item is a
button, making the thumbnails keyboard-accessible by default. - The thumbnail stores the larger image URL in
data-fulland the caption indata-caption. - When a user clicks a thumbnail, JavaScript opens the lightbox and updates the preview image.
- The previous and next buttons cycle through the gallery images using the current image index.
- The
Escapekey closes the lightbox, whileArrowLeftandArrowRightnavigate between images. - When the lightbox closes, focus returns to the thumbnail that opened it.
Usage Example
To add another image, copy one .gallery-item button and update the image URLs, alt text, caption, and label.
<button class="gallery-item" type="button" data-full="large-image.jpg" data-caption="Product preview image">
<img src="thumbnail-image.jpg" alt="Product preview image">
<span class="gallery-caption">Product Preview</span>
</button>
When to Use It
- Portfolio project galleries.
- Product image previews on ecommerce pages.
- Blog post image collections.
- Travel, food, photography, and design websites.
- Admin dashboards that need image preview functionality.
- Landing pages that need lightweight media previews without a large dependency.
Customization Notes
- Change
minmax(220px, 1fr)to control the minimum card width. - Adjust
aspect-ratio: 4 / 3;to create square, portrait, or widescreen thumbnails. - Replace the sample image URLs with your own local or CDN-hosted images.
- Add lazy loading with
loading="lazy"on gallery images for better performance. - Add image categories or filters if your gallery contains many items.
- Use
srcsetfor responsive image loading in production.
Common Mistakes
- Using clickable
divelements instead of realbuttonelements for thumbnails. - Forgetting useful
alttext for gallery images. - Loading very large images as thumbnails, which slows down the page.
- Not restoring focus after closing the lightbox.
- Not supporting the
Escapekey for closing the preview. - Using unclear captions that do not explain what the image represents.
- Forgetting to test the layout on mobile screens.
Beginner-Friendly Tips
- Start with four images before expanding the gallery.
- Use smaller thumbnail images for the grid and larger images only inside the lightbox.
- Keep image file names descriptive, such as
portfolio-dashboard-thumbnail.jpg. - Test the gallery with only your keyboard: Tab to a thumbnail, press Enter, use arrow keys, then press Escape.
Related Practice Ideas
- Add a filter menu for categories such as nature, products, and projects.
- Add a loading spinner while the large image loads.
- Add swipe support for mobile users.
- Create a masonry-style gallery layout.
- Convert this snippet into a reusable JavaScript class or ES module.
- Rebuild the same gallery in React, Vue, or Laravel Blade.

