Overview
The lightbox provides a full-screen, immersive image viewing experience with keyboard navigation, thumbnail pagination, and smooth transitions. It’s optimized for both desktop and mobile devices.
Lightbox State
The lightbox maintains its own state independently from the main gallery:
// Lightbox state
let lightboxImages = [];
let lightboxCurrentIndex = 0 ;
let lightboxThumbPage = 0 ;
const LIGHTBOX_THUMBS_PER_PAGE = 8 ;
Code location: app.js:3-7
The lightbox always displays 8 thumbnails per page, unlike the main gallery which adjusts based on screen size.
Opening the Lightbox
The openLightbox() function initializes the lightbox and displays the selected image:
const openLightbox = ( index ) => {
const lightbox = $ ( "#lightbox" );
lightboxCurrentIndex = index ;
// Calculate which page this image is on
lightboxThumbPage = Math . floor ( index / LIGHTBOX_THUMBS_PER_PAGE );
updateLightboxImage ();
renderLightboxThumbnails ();
lightbox . classList . remove ( "hidden" );
lightbox . classList . add ( "flex" );
document . body . style . overflow = "hidden" ;
};
Code location: app.js:56-66
The lightbox automatically calculates which thumbnail page to display based on the selected image index.
Closing the Lightbox
The lightbox can be closed via the close button, Escape key, or clicking the backdrop:
const closeLightbox = () => {
const lightbox = $ ( "#lightbox" );
lightbox . classList . add ( "hidden" );
lightbox . classList . remove ( "flex" );
document . body . style . overflow = "" ;
};
Code location: app.js:68-73
Image Navigation
Next/Previous Image
Navigate through images with arrow buttons or keyboard shortcuts:
const lightboxNext = () => {
lightboxCurrentIndex = ( lightboxCurrentIndex + 1 ) % lightboxImages . length ;
// Auto-advance page if needed
const newPage = Math . floor ( lightboxCurrentIndex / LIGHTBOX_THUMBS_PER_PAGE );
if ( newPage !== lightboxThumbPage ) {
lightboxThumbPage = newPage ;
renderLightboxThumbnails ();
} else {
updateLightboxImage ();
}
};
const lightboxPrev = () => {
lightboxCurrentIndex = ( lightboxCurrentIndex - 1 + lightboxImages . length ) % lightboxImages . length ;
// Auto-advance page if needed
const newPage = Math . floor ( lightboxCurrentIndex / LIGHTBOX_THUMBS_PER_PAGE );
if ( newPage !== lightboxThumbPage ) {
lightboxThumbPage = newPage ;
renderLightboxThumbnails ();
} else {
updateLightboxImage ();
}
};
Code location: app.js:144-166
Navigation wraps around: clicking “next” on the last image returns to the first image, and vice versa.
Updating the Display
The updateLightboxImage() function updates the main image and counter:
const updateLightboxImage = () => {
const lightboxImage = $ ( "#lightbox-image" );
const lightboxCounter = $ ( "#lightbox-counter" );
if ( lightboxImages . length > 0 ) {
lightboxImage . src = lightboxImages [ lightboxCurrentIndex ];
lightboxImage . alt = `Imagen ${ lightboxCurrentIndex + 1 } ` ;
lightboxCounter . textContent = ` ${ lightboxCurrentIndex + 1 } / ${ lightboxImages . length } ` ;
}
// Update thumbnail highlights
updateLightboxThumbHighlights ();
};
Code location: app.js:75-87
Thumbnail Navigation
Rendering Thumbnails
The lightbox renders its own paginated thumbnail strip:
const renderLightboxThumbnails = () => {
const container = $ ( "#lightbox-thumbnails" );
const paginationContainer = $ ( "#lightbox-pagination" );
const prevBtn = $ ( "#lightbox-thumb-prev" );
const nextBtn = $ ( "#lightbox-thumb-next" );
container . innerHTML = "" ;
const totalPages = getTotalPages ( lightboxImages . length , LIGHTBOX_THUMBS_PER_PAGE );
const startIndex = lightboxThumbPage * LIGHTBOX_THUMBS_PER_PAGE ;
const endIndex = Math . min ( startIndex + LIGHTBOX_THUMBS_PER_PAGE , lightboxImages . length );
for ( let i = startIndex ; i < endIndex ; i ++ ) {
const src = lightboxImages [ i ];
const thumb = document . createElement ( "div" );
const isActive = i === lightboxCurrentIndex ;
thumb . className = `lightbox-thumb w-12 h-12 md:w-14 md:h-14 rounded-lg overflow-hidden cursor-pointer transition-all flex-shrink-0 ${ isActive ? "ring-2 ring-white" : "opacity-50 hover:opacity-80" } ` ;
const img = document . createElement ( "img" );
img . src = src ;
img . alt = `Miniatura ${ i + 1 } ` ;
img . className = "w-full h-full object-cover" ;
thumb . appendChild ( img );
thumb . addEventListener ( "click" , () => {
lightboxCurrentIndex = i ;
updateLightboxImage ();
});
container . appendChild ( thumb );
}
// Update navigation buttons
if ( prevBtn ) prevBtn . disabled = lightboxThumbPage === 0 ;
if ( nextBtn ) nextBtn . disabled = lightboxThumbPage >= totalPages - 1 ;
// Render pagination dots
if ( paginationContainer ) {
renderPaginationDots ( paginationContainer , lightboxThumbPage , totalPages , ( page ) => {
lightboxThumbPage = page ;
renderLightboxThumbnails ();
}, true );
}
};
Code location: app.js:102-142
Highlight Active Thumbnail
const updateLightboxThumbHighlights = () => {
const thumbs = document . querySelectorAll ( "#lightbox-thumbnails .lightbox-thumb" );
const startIndex = lightboxThumbPage * LIGHTBOX_THUMBS_PER_PAGE ;
thumbs . forEach (( thumb , i ) => {
const actualIndex = startIndex + i ;
const isActive = actualIndex === lightboxCurrentIndex ;
thumb . classList . toggle ( "ring-2" , isActive );
thumb . classList . toggle ( "ring-white" , isActive );
thumb . classList . toggle ( "opacity-50" , ! isActive );
});
};
Code location: app.js:89-100
Page Navigation
const lightboxThumbNextPage = () => {
const totalPages = getTotalPages ( lightboxImages . length , LIGHTBOX_THUMBS_PER_PAGE );
if ( lightboxThumbPage < totalPages - 1 ) {
lightboxThumbPage ++ ;
renderLightboxThumbnails ();
}
};
const lightboxThumbPrevPage = () => {
if ( lightboxThumbPage > 0 ) {
lightboxThumbPage -- ;
renderLightboxThumbnails ();
}
};
Code location: app.js:168-181
Keyboard Shortcuts
The lightbox supports keyboard navigation for efficient browsing:
// Keyboard navigation
document . addEventListener ( "keydown" , ( e ) => {
if ( lightbox . classList . contains ( "hidden" )) return ;
if ( e . key === "Escape" ) closeLightbox ();
if ( e . key === "ArrowRight" ) lightboxNext ();
if ( e . key === "ArrowLeft" ) lightboxPrev ();
});
Code location: app.js:205-211
Initialization
The lightbox is initialized when the page loads:
const initLightbox = () => {
const lightbox = $ ( "#lightbox" );
const closeBtn = $ ( "#lightbox-close" );
const prevBtn = $ ( "#lightbox-prev" );
const nextBtn = $ ( "#lightbox-next" );
const thumbPrevBtn = $ ( "#lightbox-thumb-prev" );
const thumbNextBtn = $ ( "#lightbox-thumb-next" );
if ( ! lightbox ) return ;
closeBtn ?. addEventListener ( "click" , closeLightbox );
prevBtn ?. addEventListener ( "click" , lightboxPrev );
nextBtn ?. addEventListener ( "click" , lightboxNext );
thumbPrevBtn ?. addEventListener ( "click" , lightboxThumbPrevPage );
thumbNextBtn ?. addEventListener ( "click" , lightboxThumbNextPage );
// Close on backdrop click
lightbox . addEventListener ( "click" , ( e ) => {
if ( e . target === lightbox ) closeLightbox ();
});
// Keyboard navigation
document . addEventListener ( "keydown" , ( e ) => {
if ( lightbox . classList . contains ( "hidden" )) return ;
if ( e . key === "Escape" ) closeLightbox ();
if ( e . key === "ArrowRight" ) lightboxNext ();
if ( e . key === "ArrowLeft" ) lightboxPrev ();
});
};
Code location: app.js:183-212
HTML Structure
The lightbox modal overlays the entire screen with a dark backdrop:
<!-- Lightbox Modal -->
< div id = "lightbox" class = "fixed inset-0 z-[100] hidden items-center justify-center bg-black/90 backdrop-blur-sm" >
< button id = "lightbox-close" class = "absolute top-4 right-4 p-3 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors z-10" >
< span class = "material-icons-outlined text-2xl" > close </ span >
</ button >
< button id = "lightbox-prev" class = "absolute left-4 top-1/2 -translate-y-1/2 p-3 md:p-4 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors z-10" >
< span class = "material-icons-outlined text-2xl md:text-3xl" > chevron_left </ span >
</ button >
< button id = "lightbox-next" class = "absolute right-4 top-1/2 -translate-y-1/2 p-3 md:p-4 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors z-10" >
< span class = "material-icons-outlined text-2xl md:text-3xl" > chevron_right </ span >
</ button >
< div class = "relative max-w-[90vw] max-h-[70vh] md:max-h-[75vh] flex items-center justify-center" >
< img id = "lightbox-image" class = "max-w-full max-h-[65vh] md:max-h-[70vh] object-contain rounded-lg shadow-2xl" src = "" alt = "" />
</ div >
<!-- Lightbox bottom controls -->
< div class = "absolute bottom-4 md:bottom-6 left-0 right-0 flex flex-col items-center gap-3" >
<!-- Thumbnails with pagination -->
< div class = "flex items-center gap-2 max-w-[95vw] md:max-w-[80vw]" >
< button id = "lightbox-thumb-prev" class = "flex-shrink-0 p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed" >
< span class = "material-icons-outlined" > chevron_left </ span >
</ button >
< div id = "lightbox-thumbnails" class = "flex gap-2 p-2 bg-black/50 rounded-xl overflow-hidden" ></ div >
< button id = "lightbox-thumb-next" class = "flex-shrink-0 p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed" >
< span class = "material-icons-outlined" > chevron_right </ span >
</ button >
</ div >
<!-- Counter and pagination dots -->
< div class = "flex items-center gap-3" >
< span id = "lightbox-counter" class = "text-white/80 text-sm font-medium bg-black/50 px-4 py-2 rounded-full" > 1 / 10 </ span >
< div id = "lightbox-pagination" class = "flex gap-1.5" ></ div >
</ div >
</ div >
</ div >
Code location: index.html:243-275
Features
Keyboard Control Navigate with arrow keys, close with Escape
Backdrop Click Click outside the image to close the lightbox
Image Counter Shows current image number and total count
Thumbnail Preview Bottom thumbnail strip with pagination for quick navigation
User Experience
Open Lightbox
Click the main gallery image or double-click any thumbnail to open the lightbox
Navigate
Use arrow buttons, keyboard arrows, or click thumbnails to browse images
Close
Press Escape, click the close button, or click outside the image to exit