UI Components Medium
Image Lightbox
Click-to-open image lightbox with overlay, zoom, keyboard navigation (←→ Escape), and swipe support on mobile.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #050910;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
width: 100%;
max-width: 540px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
/* ── Gallery grid ── */
.gallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.625rem;
}
.gallery-item {
position: relative;
aspect-ratio: 4 / 3;
border: none;
border-radius: 0.75rem;
overflow: hidden;
cursor: pointer;
padding: 0;
background: none;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.gallery-item:hover {
transform: scale(1.03);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.gallery-item:focus-visible {
outline: 2px solid #38bdf8;
outline-offset: 3px;
}
.gallery-swatch {
width: 100%;
height: 100%;
transition: filter 0.2s ease;
}
.gallery-item:hover .gallery-swatch {
filter: brightness(0.7);
}
/* Hover overlay with expand icon */
.gallery-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
opacity: 0;
transition: opacity 0.2s ease;
}
.gallery-item:hover .gallery-overlay {
opacity: 1;
}
/* ── Lightbox ── */
.lightbox {
position: fixed;
inset: 0;
z-index: 9000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.lightbox.is-open {
opacity: 1;
pointer-events: auto;
}
.lightbox-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.92);
backdrop-filter: blur(8px);
}
.lightbox-content {
position: relative;
z-index: 1;
width: min(90vw, 800px);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
/* ── Image area ── */
.lightbox-image-wrap {
width: 100%;
aspect-ratio: 16 / 10;
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.8);
transform: scale(0.92);
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.lightbox.is-open .lightbox-image-wrap {
transform: scale(1);
}
.lightbox-image {
width: 100%;
height: 100%;
background-size: cover;
transition: background 0.3s ease;
}
/* ── Counter ── */
.lightbox-counter {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.06em;
color: #475569;
}
/* ── Caption ── */
.lightbox-caption {
font-size: 0.875rem;
color: #94a3b8;
text-align: center;
min-height: 1.25rem;
}
/* ── Navigation buttons ── */
.lightbox-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 3rem;
height: 3rem;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
color: #f2f6ff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, transform 0.15s;
backdrop-filter: blur(8px);
}
.lightbox-btn:hover {
background: rgba(255, 255, 255, 0.16);
}
.lightbox-btn:focus-visible {
outline: 2px solid #38bdf8;
outline-offset: 2px;
}
.lightbox-prev {
left: -1.5rem;
}
.lightbox-next {
right: -1.5rem;
}
@media (max-width: 600px) {
.lightbox-prev {
left: 0.5rem;
}
.lightbox-next {
right: 0.5rem;
}
}
/* ── Close button ── */
.lightbox-close {
position: absolute;
top: -3.5rem;
right: 0;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #94a3b8;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, color 0.15s;
}
.lightbox-close:hover {
background: rgba(255, 255, 255, 0.14);
color: #f2f6ff;
}
.lightbox-close:focus-visible {
outline: 2px solid #38bdf8;
outline-offset: 2px;
}
/* Body scroll lock */
body.lightbox-open {
overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
.lightbox,
.lightbox-image-wrap {
transition: none;
}
}(function () {
"use strict";
const images = [
{ bg: "linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%)", caption: "Violet Dusk" },
{ bg: "linear-gradient(135deg, #0891b2 0%, #0e7490 50%, #164e63 100%)", caption: "Ocean Deep" },
{ bg: "linear-gradient(135deg, #059669 0%, #065f46 100%)", caption: "Forest" },
{ bg: "linear-gradient(135deg, #f59e0b 0%, #dc2626 50%, #9d174d 100%)", caption: "Sunset" },
{ bg: "linear-gradient(135deg, #a78bfa 0%, #38bdf8 50%, #34d399 100%)", caption: "Aurora" },
{ bg: "linear-gradient(135deg, #ef4444 0%, #f97316 60%, #facc15 100%)", caption: "Ember" },
];
const lightbox = document.getElementById("lightbox");
const lbImage = document.getElementById("lightbox-image");
const lbCounter = document.getElementById("lightbox-counter");
const lbCaption = document.getElementById("lightbox-caption");
const lbPrev = document.getElementById("lightbox-prev");
const lbNext = document.getElementById("lightbox-next");
const lbClose = document.getElementById("lightbox-close");
const lbBackdrop = document.getElementById("lightbox-backdrop");
let currentIndex = 0;
let lastFocused = null;
function open(index) {
lastFocused = document.activeElement;
currentIndex = index;
render();
lightbox.classList.add("is-open");
lightbox.setAttribute("aria-hidden", "false");
document.body.classList.add("lightbox-open");
lbClose.focus();
}
function close() {
lightbox.classList.remove("is-open");
lightbox.setAttribute("aria-hidden", "true");
document.body.classList.remove("lightbox-open");
if (lastFocused) lastFocused.focus();
}
function prev() {
currentIndex = (currentIndex - 1 + images.length) % images.length;
render();
}
function next() {
currentIndex = (currentIndex + 1) % images.length;
render();
}
function render() {
const img = images[currentIndex];
lbImage.style.background = img.bg;
lbCounter.textContent = currentIndex + 1 + " / " + images.length;
lbCaption.textContent = img.caption;
}
// Open from gallery
document.querySelectorAll(".gallery-item").forEach(function (item) {
item.addEventListener("click", function () {
open(Number(item.dataset.index));
});
});
// Controls
lbPrev.addEventListener("click", prev);
lbNext.addEventListener("click", next);
lbClose.addEventListener("click", close);
lbBackdrop.addEventListener("click", close);
// Keyboard
document.addEventListener("keydown", function (e) {
if (!lightbox.classList.contains("is-open")) return;
if (e.key === "Escape") {
e.preventDefault();
close();
}
if (e.key === "ArrowLeft") {
e.preventDefault();
prev();
}
if (e.key === "ArrowRight") {
e.preventDefault();
next();
}
});
// Focus trap
lightbox.addEventListener("keydown", function (e) {
if (e.key !== "Tab") return;
const focusable = Array.from(lightbox.querySelectorAll("button"));
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
// Touch / swipe support
let touchStartX = 0;
lightbox.addEventListener(
"touchstart",
function (e) {
touchStartX = e.touches[0].clientX;
},
{ passive: true }
);
lightbox.addEventListener(
"touchend",
function (e) {
const dx = e.changedTouches[0].clientX - touchStartX;
if (Math.abs(dx) < 50) return;
dx < 0 ? next() : prev();
},
{ passive: true }
);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Image Lightbox</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Image Lightbox</h1>
<p class="demo-sub">Click any image to open the lightbox. Use ← → to navigate.</p>
<!-- Gallery grid -->
<div class="gallery" role="list">
<button class="gallery-item" data-index="0" role="listitem" aria-label="Open image 1: Violet Dusk">
<div class="gallery-swatch" style="background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%);"></div>
<div class="gallery-overlay">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
</div>
</button>
<button class="gallery-item" data-index="1" role="listitem" aria-label="Open image 2: Ocean Deep">
<div class="gallery-swatch" style="background: linear-gradient(135deg, #0891b2 0%, #0e7490 50%, #164e63 100%);"></div>
<div class="gallery-overlay">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
</div>
</button>
<button class="gallery-item" data-index="2" role="listitem" aria-label="Open image 3: Forest">
<div class="gallery-swatch" style="background: linear-gradient(135deg, #059669 0%, #065f46 100%);"></div>
<div class="gallery-overlay">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
</div>
</button>
<button class="gallery-item" data-index="3" role="listitem" aria-label="Open image 4: Sunset">
<div class="gallery-swatch" style="background: linear-gradient(135deg, #f59e0b 0%, #dc2626 50%, #9d174d 100%);"></div>
<div class="gallery-overlay">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
</div>
</button>
<button class="gallery-item" data-index="4" role="listitem" aria-label="Open image 5: Aurora">
<div class="gallery-swatch" style="background: linear-gradient(135deg, #a78bfa 0%, #38bdf8 50%, #34d399 100%);"></div>
<div class="gallery-overlay">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
</div>
</button>
<button class="gallery-item" data-index="5" role="listitem" aria-label="Open image 6: Ember">
<div class="gallery-swatch" style="background: linear-gradient(135deg, #ef4444 0%, #f97316 60%, #facc15 100%);"></div>
<div class="gallery-overlay">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
</div>
</button>
</div>
</div>
<!-- Lightbox modal -->
<div
class="lightbox"
id="lightbox"
role="dialog"
aria-modal="true"
aria-label="Image lightbox"
aria-hidden="true"
tabindex="-1"
>
<!-- Backdrop -->
<div class="lightbox-backdrop" id="lightbox-backdrop"></div>
<!-- Content -->
<div class="lightbox-content">
<!-- Image display -->
<div class="lightbox-image-wrap">
<div class="lightbox-image" id="lightbox-image"></div>
</div>
<!-- Counter -->
<div class="lightbox-counter" id="lightbox-counter" aria-live="polite">1 / 6</div>
<!-- Caption -->
<p class="lightbox-caption" id="lightbox-caption"></p>
<!-- Controls -->
<button class="lightbox-btn lightbox-prev" id="lightbox-prev" aria-label="Previous image">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M15 18l-6-6 6-6"/>
</svg>
</button>
<button class="lightbox-btn lightbox-next" id="lightbox-next" aria-label="Next image">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M9 18l6-6-6-6"/>
</svg>
</button>
<button class="lightbox-close" id="lightbox-close" aria-label="Close lightbox">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Image Lightbox
A zero-dependency image lightbox with smooth open/close transitions, keyboard navigation, and mobile swipe support. Click any image in the gallery to open it full-screen.
Features
- Grid gallery with hover overlay
- Smooth fade+scale open/close transition
- Keyboard navigation:
←/→to navigate,Escapeto close - Touch swipe support (left/right) on mobile
- Counter indicator (e.g., “3 / 6”)
- Traps focus inside the lightbox when open
aria-modaldialog pattern with backdrop click-to-close- Body scroll locked while lightbox is open
Keyboard shortcuts
| Key | Action |
|---|---|
← | Previous image |
→ | Next image |
Escape | Close lightbox |
Swipe
Swipe left to go to the next image, swipe right to go to the previous image. Minimum swipe distance: 50px.