Creating an engaging and interactive photo gallery is essential for websites that showcase visual content, whether for portfolios, e-commerce, or personal blogs. A lightbox photo gallery enhances user experience by allowing users to view images and videos in a full-screen overlay without navigating away from the page. Adding keyboard navigation and volume control for videos makes the gallery accessible and user-friendly, catering to both desktop and mobile users. This comprehensive guide will walk you through the process of building a modern lightbox photo gallery using HTML, CSS, and JavaScript, with features like keyboard navigation, swipe gestures, and volume control for video content. We'll also explore free AI tools to enhance your development process and provide a complete code example.
A lightbox gallery displays media in a modal overlay, dimming the background to focus on the content. It’s ideal for:
Popular libraries like LightGallery and PhotoSwipe offer robust solutions, but building a custom lightbox allows for greater flexibility and lightweight code tailored to your needs.
Our lightbox gallery will include:
The gallery consists of a grid of thumbnails (images and videos) and a lightbox overlay. Each thumbnail links to a larger media file, with metadata like captions stored in data-
attributes.
<div class="gallery">
<img src="image1.jpg" alt="Image 1" data-caption="First Image">
<video src="video1.mp4" poster="video1-poster.jpg" muted data-caption="Sample Video"></video>
</div>
<div class="lightbox" id="lightbox">
<div class="lightbox-content">
<span class="close" id="close">×</span>
<span class="prev" id="prev">❮</span>
<span class="next" id="next">❯</span>
<div id="media-container"></div>
<div class="caption" id="caption"></div>
<div class="volume-control" id="volume-control">
<label for="volume">Volume:</label>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1">
</div>
</div>
</div>
The CSS ensures a responsive grid layout for thumbnails and a centered lightbox with navigation controls. Media queries handle mobile responsiveness.
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
}
.lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
justify-content: center;
align-items: center;
}
.lightbox img, .lightbox video {
max-width: 90%;
max-height: 80vh;
object-fit: contain;
}
.volume-control {
display: none;
position: absolute;
bottom: 20px;
left: 20px;
color: white;
}
@media (max-width: 600px) {
.gallery {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
The JavaScript handles lightbox opening/closing, navigation, swipe gestures, and volume control. Keyboard events are added for accessibility.
const galleryItems = document.querySelectorAll('.gallery img, .gallery video');
const lightbox = document.getElementById('lightbox');
let currentIndex = 0;
function openLightbox(index) {
currentIndex = index;
showMedia(currentIndex);
lightbox.style.display = 'flex';
document.addEventListener('keydown', handleKeydown);
}
function showMedia(index) {
const item = galleryItems[index];
const mediaContainer = document.getElementById('media-container');
mediaContainer.innerHTML = '';
if (item.tagName === 'VIDEO') {
const video = document.createElement('video');
video.src = item.src;
video.controls = true;
video.autoplay = true;
mediaContainer.appendChild(video);
document.getElementById('volume-control').style.display = 'block';
} else {
const img = document.createElement('img');
img.src = item.src;
mediaContainer.appendChild(img);
document.getElementById('volume-control').style.display = 'none';
}
}
Keyboard navigation enhances accessibility. Use left/right arrows for navigation, Escape to close, and up/down arrows for volume control.
function handleKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
currentIndex = (currentIndex - 1 + galleryItems.length) % galleryItems.length;
showMedia(currentIndex);
break;
case 'ArrowRight':
currentIndex = (currentIndex + 1) % galleryItems.length;
showMedia(currentIndex);
break;
case 'Escape':
lightbox.style.display = 'none';
document.removeEventListener('keydown', handleKeydown);
break;
case 'ArrowUp':
if (currentVideo) currentVideo.volume = Math.min(currentVideo.volume + 0.1, 1);
break;
case 'ArrowDown':
if (currentVideo) currentVideo.volume = Math.max(currentVideo.volume - 0.1, 0);
break;
}
}
For mobile users, swipe gestures are implemented using touch events.
let touchStartX = 0;
lightbox.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
});
lightbox.addEventListener('touchend', (e) => {
const touchEndX = e.changedTouches[0].screenX;
if (touchStartX - touchEndX > 50) {
currentIndex = (currentIndex + 1) % galleryItems.length;
showMedia(currentIndex);
} else if (touchEndX - touchStartX > 50) {
currentIndex = (currentIndex - 1 + galleryItems.length) % galleryItems.length;
showMedia(currentIndex);
}
});
To streamline development, consider using free AI tools:
aria-label
to navigation buttons for screen readers.Below is the complete code for the lightbox gallery, including all features discussed. The image sources use Picsum Photos for reliable placeholders, and the video uses a sample from W3Schools.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lightbox Photo Gallery</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f0f0;
}
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
max-width: 1200px;
margin: 0 auto;
}
.gallery img, .gallery video {
width: 100%;
height: 150px;
object-fit: cover;
cursor: pointer;
border-radius: 5px;
}
.lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
justify-content: center;
align-items: center;
z-index: 1000;
}
.lightbox-content {
max-width: 90%;
max-height: 90%;
position: relative;
}
.lightbox img, .lightbox video {
width: 100%;
height: auto;
max-height: 80vh;
object-fit: contain;
}
.close, .prev, .next {
position: absolute;
color: white;
font-size: 30px;
cursor: pointer;
user-select: none;
}
.close {
top: 20px;
right: 20px;
}
.prev {
left: 20px;
top: 50%;
transform: translateY(-50%);
}
.next {
right: 20px;
top: 50%;
transform: translateY(-50%);
}
.caption {
color: white;
text-align: center;
margin-top: 10px;
font-size: 16px;
}
.volume-control {
position: absolute;
bottom: 20px;
left: 20px;
color: white;
display: none;
}
.volume-control input {
width: 100px;
}
@media (max-width: 600px) {
.gallery {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.close, .prev, .next {
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="gallery">
<img src="https://picsum.photos/800/600?random=1" alt="Image 1" data-caption="First Image">
<img src="https://picsum.photos/800/600?random=2" alt="Image 2" data-caption="Second Image">
<video src="https://www.w3schools.com/html/mov_bbb.mp4" poster="https://picsum.photos/800/600?random=3" muted data-caption="Sample Video"></video>
<img src="https://picsum.photos/800/600?random=4" alt="Image 3" data-caption="Third Image">
</div>
<div class="lightbox" id="lightbox">
<div class="lightbox-content">
<span class="close" id="close" aria-label="Close lightbox">×</span>
<span class="prev" id="prev" aria-label="Previous media">❮</span>
<span class="next" id="next" aria-label="Next media">❯</span>
<div id="media-container"></div>
<div class="caption" id="caption"></div>
<div class="volume-control" id="volume-control">
<label for="volume">Volume:</label>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1">
</div>
</div>
</div>
<script>
const galleryItems = document.querySelectorAll('.gallery img, .gallery video');
const lightbox = document.getElementById('lightbox');
const mediaContainer = document.getElementById('media-container');
const caption = document.getElementById('caption');
const closeBtn = document.getElementById('close');
const prevBtn = document.getElementById('prev');
const nextBtn = document.getElementById('next');
const volumeControl = document.getElementById('volume-control');
const volumeSlider = document.getElementById('volume');
let currentIndex = 0;
let currentVideo = null;
let touchStartX = 0;
function openLightbox(index) {
currentIndex = index;
showMedia(currentIndex);
lightbox.style.display = 'flex';
document.addEventListener('keydown', handleKeydown);
}
function closeLightbox() {
lightbox.style.display = 'none';
if (currentVideo) {
currentVideo.pause();
currentVideo = null;
}
volumeControl.style.display = 'none';
document.removeEventListener('keydown', handleKeydown);
}
function showMedia(index) {
const item = galleryItems[index];
const isVideo = item.tagName === 'VIDEO';
mediaContainer.innerHTML = '';
if (isVideo) {
const video = document.createElement('video');
video.src = item.src;
video.controls = true;
video.autoplay = true;
mediaContainer.appendChild(video);
currentVideo = video;
volumeControl.style.display = 'block';
volumeSlider.value = video.volume;
video.addEventListener('volumechange', () => {
volumeSlider.value = video.volume;
});
volumeSlider.addEventListener('input', () => {
video.volume = volumeSlider.value;
});
} else {
const img = document.createElement('img');
img.src = item.src;
img.alt = item.alt;
mediaContainer.appendChild(img);
volumeControl.style.display = 'none';
}
caption.textContent = item.dataset.caption || '';
}
function handleKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
currentIndex = (currentIndex - 1 + galleryItems.length) % galleryItems.length;
showMedia(currentIndex);
break;
case 'ArrowRight':
currentIndex = (currentIndex + 1) % galleryItems.length;
showMedia(currentIndex);
break;
case 'Escape':
closeLightbox();
break;
case 'ArrowUp':
if (currentVideo) {
currentVideo.volume = Math.min(currentVideo.volume + 0.1, 1);
}
break;
case 'ArrowDown':
if (currentVideo) {
currentVideo.volume = Math.max(currentVideo.volume - 0.1, 0);
}
break;
}
}
galleryItems.forEach((item, index) => {
item.addEventListener('click', () => openLightbox(index));
});
closeBtn.addEventListener('click', closeLightbox);
prevBtn.addEventListener('click', () => {
currentIndex = (currentIndex - 1 + galleryItems.length) % galleryItems.length;
showMedia(currentIndex);
});
nextBtn.addEventListener('click', () => {
currentIndex = (currentIndex + 1) % galleryItems.length;
showMedia(currentIndex);
});
lightbox.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
});
lightbox.addEventListener('touchend', (e) => {
const touchEndX = e.changedTouches[0].screenX;
if (touchStartX - touchEndX > 50) {
currentIndex = (currentIndex + 1) % galleryItems.length;
showMedia(currentIndex);
} else if (touchEndX - touchStartX > 50) {
currentIndex = (currentIndex - 1 + galleryItems.length) % galleryItems.length;
showMedia(currentIndex);
}
});
</script>
</body>
</html>
Building a lightbox photo gallery with keyboard navigation and volume control is a rewarding project that enhances user experience and accessibility. By using vanilla JavaScript, you maintain control over functionality without relying on heavy libraries. Free AI tools like Grok can assist in debugging and optimizing your code, while platforms like CodePen allow for quick prototyping. Implement the code above, customize it to your needs, and create a stunning gallery for your website.