Lightbox
Fullscreen image viewer with navigation, thumbnails, zoom controls, captions, and keyboard support.
Basic Lightbox
Preview
Mountain Landscape
<!-- Trigger -->
<button class="lightbox-trigger" onclick="document.getElementById('lb-demo').classList.add('lightbox-open')">
<img src="https://picsum.photos/300/200?random=1" alt="Sample" class="rounded-lg cursor-pointer" style="max-width: 300px;" />
</button>
<!-- Lightbox -->
<div id="lb-demo" class="lightbox">
<div class="lightbox-backdrop"></div>
<div class="lightbox-container">
<div class="lightbox-header">
<span class="lightbox-title">Mountain Landscape</span>
<div class="lightbox-actions">
<button class="lightbox-btn" onclick="this.closest('.lightbox').classList.remove('lightbox-open')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
<div class="lightbox-image-container">
<div class="lightbox-image-wrapper">
<img src="https://picsum.photos/1200/800?random=1" alt="Mountain Landscape" class="lightbox-image" />
</div>
</div>
</div>
</div> Requires: ux.min.css
<!-- Trigger -->
<button onclick="document.getElementById('lb-tw').classList.add('!opacity-100','!visible')">
<img src="https://picsum.photos/300/200?random=1" alt="Sample" class="rounded-lg cursor-pointer" style="max-width: 300px;" />
</button>
<!-- Lightbox -->
<div id="lb-tw" class="fixed inset-0 z-500 flex items-center justify-center opacity-0 invisible transition-all duration-200">
<div class="absolute inset-0 bg-black/95 -z-1"></div>
<div class="relative w-full h-full flex flex-col">
<div class="absolute top-0 left-0 right-0 flex items-center justify-between p-3 z-10" style="background: linear-gradient(to bottom, rgba(0,0,0,0.5), transparent);">
<span class="text-base font-medium text-white truncate">Mountain Landscape</span>
<div class="flex items-center gap-1">
<button class="flex items-center justify-center size-11 text-white border-none rounded-full cursor-pointer bg-white/10 hover:bg-white/20" onclick="this.closest('[id]').classList.remove('!opacity-100','!visible')">
<svg class="size-[22px]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
<div class="flex-1 flex items-center justify-center overflow-hidden p-4 cursor-grab active:cursor-grabbing">
<div class="relative max-w-full max-h-full flex items-center justify-center">
<img src="https://picsum.photos/1200/800?random=1" alt="Mountain Landscape" class="max-w-full select-none rounded-sm" style="max-height: calc(100dvh - 120px); object-fit: contain;" />
</div>
</div>
</div>
</div> Requires: tw.min.css
// Open lightbox:
document.getElementById('lb-demo').classList.add('lightbox-open');
// Close lightbox:
document.getElementById('lb-demo').classList.remove('lightbox-open');
// Close on Escape:
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelector('.lightbox-open')?.classList.remove('lightbox-open');
}
}); Navigation & Counter
Preview
2 / 5
Coastal Sunset
Beautiful sunset view from the cliffs.
<div style="position: relative; height: 350px; background: #000; border-radius: var(--radius-box, 1rem); overflow: hidden;">
<div class="lightbox-container" style="position: absolute; inset: 0;">
<div class="lightbox-header">
<span class="lightbox-counter">2 / 5</span>
<div class="lightbox-actions">
<button class="lightbox-btn" title="Download">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>
</button>
<button class="lightbox-btn" title="Close">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
<div class="lightbox-image-container">
<button class="lightbox-nav lightbox-nav-prev">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"/></svg>
</button>
<div class="lightbox-image-wrapper">
<img src="https://picsum.photos/800/500?random=2" alt="Photo 2 of 5" class="lightbox-image" />
</div>
<button class="lightbox-nav lightbox-nav-next">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"/></svg>
</button>
</div>
<div class="lightbox-footer">
<div class="lightbox-caption">
<div class="lightbox-caption-title">Coastal Sunset</div>
<div class="lightbox-caption-description">Beautiful sunset view from the cliffs.</div>
</div>
</div>
</div>
</div> Requires: ux.min.css
<div style="position: relative; height: 350px; background: #000; border-radius: 1rem; overflow: hidden;">
<div class="absolute inset-0 w-full h-full flex flex-col">
<div class="absolute top-0 left-0 right-0 flex items-center justify-between p-3 z-10" style="background: linear-gradient(to bottom, rgba(0,0,0,0.5), transparent);">
<span class="text-sm text-white/70">2 / 5</span>
<div class="flex items-center gap-1">
<button class="flex items-center justify-center size-11 text-white border-none rounded-full cursor-pointer bg-white/10 hover:bg-white/20" title="Download">
<svg class="size-[22px]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>
</button>
<button class="flex items-center justify-center size-11 text-white border-none rounded-full cursor-pointer bg-white/10 hover:bg-white/20" title="Close">
<svg class="size-[22px]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
<div class="flex-1 flex items-center justify-center overflow-hidden p-4 relative">
<button class="absolute top-1/2 -translate-y-1/2 left-3 flex items-center justify-center size-12 bg-black/50 text-white border-none rounded-full cursor-pointer z-10 hover:bg-black/70">
<svg class="size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"/></svg>
</button>
<div class="relative max-w-full max-h-full flex items-center justify-center">
<img src="https://picsum.photos/800/500?random=2" alt="Photo 2 of 5" class="max-w-full select-none rounded-sm" style="max-height: calc(100% - 40px); object-fit: contain;" />
</div>
<button class="absolute top-1/2 -translate-y-1/2 right-3 flex items-center justify-center size-12 bg-black/50 text-white border-none rounded-full cursor-pointer z-10 hover:bg-black/70">
<svg class="size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"/></svg>
</button>
</div>
<div class="absolute bottom-0 left-0 right-0 p-3 z-10" style="background: linear-gradient(to top, rgba(0,0,0,0.5), transparent);">
<div class="text-center text-sm text-white mx-auto max-w-[600px]">
<div class="font-medium mb-1">Coastal Sunset</div>
<div class="text-white/70">Beautiful sunset view from the cliffs.</div>
</div>
</div>
</div>
</div> Requires: tw.min.css
// Navigate with arrow keys:
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') prevImage();
if (e.key === 'ArrowRight') nextImage();
});
// Navigate buttons:
document.querySelector('.lightbox-nav-prev').onclick = prevImage;
document.querySelector('.lightbox-nav-next').onclick = nextImage; Thumbnails
Preview
1 / 4
<div style="position: relative; height: 380px; background: #000; border-radius: var(--radius-box, 1rem); overflow: hidden;">
<div class="lightbox-container" style="position: absolute; inset: 0;">
<div class="lightbox-header">
<span class="lightbox-counter">1 / 4</span>
<div class="lightbox-actions">
<button class="lightbox-btn">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
<div class="lightbox-image-container">
<div class="lightbox-image-wrapper">
<img src="https://picsum.photos/800/500?random=10" alt="Photo 1" class="lightbox-image" />
</div>
</div>
<div class="lightbox-footer">
<div class="lightbox-thumbnails">
<button class="lightbox-thumbnail lightbox-thumbnail-active">
<img src="https://picsum.photos/80/80?random=10" alt="Thumb 1" />
</button>
<button class="lightbox-thumbnail">
<img src="https://picsum.photos/80/80?random=11" alt="Thumb 2" />
</button>
<button class="lightbox-thumbnail">
<img src="https://picsum.photos/80/80?random=12" alt="Thumb 3" />
</button>
<button class="lightbox-thumbnail">
<img src="https://picsum.photos/80/80?random=13" alt="Thumb 4" />
</button>
</div>
</div>
</div>
</div> Requires: ux.min.css
<div style="position: relative; height: 380px; background: #000; border-radius: 1rem; overflow: hidden;">
<div class="absolute inset-0 w-full h-full flex flex-col">
<div class="absolute top-0 left-0 right-0 flex items-center justify-between p-3 z-10" style="background: linear-gradient(to bottom, rgba(0,0,0,0.5), transparent);">
<span class="text-sm text-white/70">1 / 4</span>
<div class="flex items-center gap-1">
<button class="flex items-center justify-center size-11 text-white border-none rounded-full cursor-pointer bg-white/10 hover:bg-white/20">
<svg class="size-5.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
<div class="flex-1 flex items-center justify-center overflow-hidden p-4">
<div class="relative max-w-full max-h-full flex items-center justify-center">
<img src="https://picsum.photos/800/500?random=10" alt="Photo 1" class="max-w-full select-none rounded-sm" style="max-height: calc(100% - 40px); object-fit: contain;" />
</div>
</div>
<div class="absolute bottom-0 left-0 right-0 p-3 z-10" style="background: linear-gradient(to top, rgba(0,0,0,0.5), transparent);">
<div class="flex gap-1 justify-center overflow-x-auto py-1 scrollbar-hide">
<button class="shrink-0 size-12 rounded-sm overflow-hidden cursor-pointer border-2 border-white opacity-100">
<img src="https://picsum.photos/80/80?random=10" alt="Thumb 1" class="size-full object-cover" />
</button>
<button class="shrink-0 size-12 rounded-sm overflow-hidden cursor-pointer border-2 border-transparent opacity-50 hover:opacity-80">
<img src="https://picsum.photos/80/80?random=11" alt="Thumb 2" class="size-full object-cover" />
</button>
<button class="shrink-0 size-12 rounded-sm overflow-hidden cursor-pointer border-2 border-transparent opacity-50 hover:opacity-80">
<img src="https://picsum.photos/80/80?random=12" alt="Thumb 3" class="size-full object-cover" />
</button>
<button class="shrink-0 size-12 rounded-sm overflow-hidden cursor-pointer border-2 border-transparent opacity-50 hover:opacity-80">
<img src="https://picsum.photos/80/80?random=13" alt="Thumb 4" class="size-full object-cover" />
</button>
</div>
</div>
</div>
</div> Requires: tw.min.css
// Switch active thumbnail:
thumbnails.forEach((thumb, i) => {
thumb.onclick = () => {
document.querySelector('.lightbox-thumbnail-active')?.classList.remove('lightbox-thumbnail-active');
thumb.classList.add('lightbox-thumbnail-active');
showImage(i);
};
}); Zoom Controls
Preview
Zoomable Image
100%
<div style="position: relative; height: 350px; background: #000; border-radius: var(--radius-box, 1rem); overflow: hidden;">
<div class="lightbox-container" style="position: absolute; inset: 0;">
<div class="lightbox-header">
<span class="lightbox-title">Zoomable Image</span>
<div class="lightbox-actions">
<button class="lightbox-btn">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
<div class="lightbox-image-container">
<div class="lightbox-image-wrapper">
<img src="https://picsum.photos/800/500?random=20" alt="Zoomable" class="lightbox-image" />
</div>
</div>
<div class="lightbox-zoom-controls">
<button class="lightbox-btn" title="Zoom in">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
</button>
<span class="lightbox-zoom-level">100%</span>
<button class="lightbox-btn" title="Zoom out">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14"/></svg>
</button>
</div>
</div>
</div> Requires: ux.min.css
<div style="position: relative; height: 350px; background: #000; border-radius: 1rem; overflow: hidden;">
<div class="absolute inset-0 w-full h-full flex flex-col">
<div class="absolute top-0 left-0 right-0 flex items-center justify-between p-3 z-10" style="background: linear-gradient(to bottom, rgba(0,0,0,0.5), transparent);">
<span class="text-base font-medium text-white truncate">Zoomable Image</span>
<div class="flex items-center gap-1">
<button class="flex items-center justify-center size-11 text-white border-none rounded-full cursor-pointer bg-white/10 hover:bg-white/20">
<svg class="size-5.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
<div class="flex-1 flex items-center justify-center overflow-hidden p-4">
<div class="relative max-w-full max-h-full flex items-center justify-center">
<img src="https://picsum.photos/800/500?random=20" alt="Zoomable" class="max-w-full select-none rounded-sm" style="max-height: calc(100% - 40px); object-fit: contain;" />
</div>
</div>
<div class="absolute flex flex-col gap-1 z-10 bottom-20 right-3">
<button class="flex items-center justify-center size-11 text-white border-none rounded-full cursor-pointer bg-white/10 hover:bg-white/20" title="Zoom in">
<svg class="size-5.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
</button>
<span class="text-xs text-center text-white bg-black/50 rounded-full px-2 py-0.5">100%</span>
<button class="flex items-center justify-center size-11 text-white border-none rounded-full cursor-pointer bg-white/10 hover:bg-white/20" title="Zoom out">
<svg class="size-5.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14"/></svg>
</button>
</div>
</div>
</div> Requires: tw.min.css
// Zoom in/out with transform scale:
let zoom = 1;
function zoomIn() { zoom = Math.min(zoom + 0.25, 5); applyZoom(); }
function zoomOut() { zoom = Math.max(zoom - 0.25, 0.5); applyZoom(); }
function applyZoom() {
wrapper.style.transform = `scale(${zoom})`;
zoomLabel.textContent = Math.round(zoom * 100) + '%';
} Loading State
Preview
<div style="position: relative; height: 250px; background: #000; border-radius: var(--radius-box, 1rem); overflow: hidden;">
<div class="lightbox-container" style="position: absolute; inset: 0;">
<div class="lightbox-image-container">
<div class="lightbox-loading">
<div class="lightbox-spinner"></div>
</div>
</div>
</div>
</div> Requires: ux.min.css
<div class="flex items-center justify-center h-[250px] bg-black rounded-box">
<div class="size-10 rounded-full animate-spin" style="border: 3px solid rgba(255,255,255,0.2); border-top-color: white;"></div>
</div> Requires: tw.min.css
// Show spinner while image loads:
const img = new Image();
img.onload = () => {
spinner.style.display = 'none';
container.appendChild(img);
};
img.src = imageUrl; Trigger Element
Preview
<div class="flex gap-3">
<div class="lightbox-trigger">
<img src="https://picsum.photos/150/150?random=30" alt="Click to view" class="rounded-lg" style="width: 150px; height: 150px; object-fit: cover;" />
</div>
<div class="lightbox-trigger">
<img src="https://picsum.photos/150/150?random=31" alt="Click to view" class="rounded-lg" style="width: 150px; height: 150px; object-fit: cover;" />
</div>
<div class="lightbox-trigger">
<img src="https://picsum.photos/150/150?random=32" alt="Click to view" class="rounded-lg" style="width: 150px; height: 150px; object-fit: cover;" />
</div>
</div> Requires: ux.min.css
<div class="flex gap-3">
<div class="cursor-pointer focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-2">
<img src="https://picsum.photos/150/150?random=30" alt="Click to view" class="rounded-lg size-[150px] object-cover" />
</div>
<div class="cursor-pointer focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-2">
<img src="https://picsum.photos/150/150?random=31" alt="Click to view" class="rounded-lg size-[150px] object-cover" />
</div>
</div> Requires: tw.min.css
// Wire triggers to lightbox:
document.querySelectorAll('.lightbox-trigger').forEach((trigger, i) => {
trigger.onclick = () => { showImage(i); openLightbox(); };
});