UI Components Medium
Image Gallery Grid
Masonry photo gallery with CSS columns, hover zoom, and a lightbox with keyboard navigation and swipe support.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0d1117;
color: #e6edf3;
min-height: 100vh;
padding: 32px 16px;
display: flex;
justify-content: center;
}
.demo {
width: 100%;
max-width: 960px;
}
/* Toolbar */
.gallery-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
gap: 12px;
flex-wrap: wrap;
}
.gallery-filters {
display: flex;
gap: 6px;
}
.gf-btn {
background: #21262d;
border: 1px solid #30363d;
color: #8b949e;
font-size: 12px;
font-weight: 600;
padding: 6px 14px;
border-radius: 20px;
cursor: pointer;
transition: all 0.15s;
}
.gf-btn:hover {
color: #e6edf3;
border-color: #8b949e;
}
.gf-btn.active {
background: #6366f1;
border-color: #6366f1;
color: #fff;
}
.gallery-count {
font-size: 12px;
color: #6c7086;
}
/* Masonry grid */
.gallery-grid {
columns: 3 200px;
gap: 12px;
}
.gallery-item {
break-inside: avoid;
margin-bottom: 12px;
border-radius: 10px;
overflow: hidden;
cursor: pointer;
position: relative;
background: #161b22;
border: 1px solid #21262d;
}
.gallery-item img {
display: block;
width: 100%;
height: auto;
transition: transform 0.3s ease, opacity 0.2s;
}
.gallery-item:hover img {
transform: scale(1.04);
}
.gallery-overlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, transparent 60%);
opacity: 0;
transition: opacity 0.2s;
display: flex;
align-items: flex-end;
padding: 12px;
}
.gallery-item:hover .gallery-overlay {
opacity: 1;
}
.gallery-caption {
font-size: 12px;
font-weight: 600;
color: #fff;
}
.gallery-item.hidden {
display: none;
}
/* Lightbox */
.lightbox {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.92);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
animation: lb-in 0.2s ease;
}
.lightbox[hidden] {
display: none !important;
}
@keyframes lb-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.lb-img-wrap {
max-width: min(90vw, 900px);
max-height: 80vh;
display: flex;
align-items: center;
justify-content: center;
}
.lb-img {
max-width: 100%;
max-height: 80vh;
border-radius: 10px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
object-fit: contain;
}
.lb-close {
position: fixed;
top: 20px;
right: 24px;
background: rgba(255, 255, 255, 0.1);
border: none;
color: #fff;
font-size: 20px;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
transition: background 0.15s;
z-index: 1001;
pointer-events: auto;
}
.lb-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.lb-prev,
.lb-next {
position: fixed;
top: 50%;
transform: translateY(-50%);
background: rgba(255, 255, 255, 0.1);
border: none;
color: #fff;
font-size: 36px;
width: 48px;
height: 64px;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s;
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
}
.lb-prev {
left: 16px;
}
.lb-next {
right: 16px;
}
.lb-prev:hover,
.lb-next:hover {
background: rgba(255, 255, 255, 0.2);
}
.lb-caption {
position: fixed;
bottom: 48px;
left: 50%;
transform: translateX(-50%);
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
font-weight: 600;
}
.lb-counter {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
}const IMAGES = [
{
id: 1,
src: "https://picsum.photos/seed/forest1/600/800",
category: "nature",
caption: "Forest Path",
},
{
id: 2,
src: "https://picsum.photos/seed/arch1/700/500",
category: "architecture",
caption: "Urban Lines",
},
{
id: 3,
src: "https://picsum.photos/seed/abstract1/500/600",
category: "abstract",
caption: "Color Flow",
},
{
id: 4,
src: "https://picsum.photos/seed/mountain1/600/400",
category: "nature",
caption: "Mountain View",
},
{
id: 5,
src: "https://picsum.photos/seed/building1/500/700",
category: "architecture",
caption: "Glass Tower",
},
{
id: 6,
src: "https://picsum.photos/seed/geo1/600/600",
category: "abstract",
caption: "Geometry",
},
{
id: 7,
src: "https://picsum.photos/seed/lake1/700/450",
category: "nature",
caption: "Still Lake",
},
{
id: 8,
src: "https://picsum.photos/seed/bridge1/600/400",
category: "architecture",
caption: "Steel Bridge",
},
{
id: 9,
src: "https://picsum.photos/seed/pattern1/400/600",
category: "abstract",
caption: "Pattern Study",
},
{
id: 10,
src: "https://picsum.photos/seed/canyon1/700/500",
category: "nature",
caption: "Canyon Walls",
},
{
id: 11,
src: "https://picsum.photos/seed/skyline1/800/500",
category: "architecture",
caption: "City Skyline",
},
{
id: 12,
src: "https://picsum.photos/seed/texture1/500/500",
category: "abstract",
caption: "Texture",
},
];
let activeFilter = "all";
let lightboxIndex = 0;
let visibleImages = [...IMAGES];
function getVisibleIndex(img) {
let idx = visibleImages.indexOf(img);
if (idx >= 0) return idx;
return visibleImages.findIndex((i) => i.id === img.id);
}
function buildGrid() {
const grid = document.getElementById("galleryGrid");
const countEl = document.getElementById("galleryCount");
if (!grid || !countEl) return;
grid.innerHTML = "";
visibleImages = IMAGES.filter((img) => activeFilter === "all" || img.category === activeFilter);
countEl.textContent = `${visibleImages.length} photos`;
IMAGES.forEach((img) => {
const item = document.createElement("div");
item.className =
"gallery-item" + (activeFilter !== "all" && img.category !== activeFilter ? " hidden" : "");
item.dataset.id = img.id;
const el = document.createElement("img");
el.src = img.src;
el.alt = img.caption;
el.loading = "lazy";
const overlay = document.createElement("div");
overlay.className = "gallery-overlay";
const cap = document.createElement("span");
cap.className = "gallery-caption";
cap.textContent = img.caption;
overlay.appendChild(cap);
item.appendChild(el);
item.appendChild(overlay);
item.addEventListener("click", () => {
const idx = getVisibleIndex(img);
if (idx >= 0) openLightbox(idx);
});
grid.appendChild(item);
});
}
function openLightbox(index) {
if (index < 0 || index >= visibleImages.length) return;
lightboxIndex = index;
const lb = document.getElementById("lightbox");
if (!lb) return;
lb.hidden = false;
updateLightbox();
}
function updateLightbox() {
const img = visibleImages[lightboxIndex];
const lbImg = document.getElementById("lbImg");
const lbCaption = document.getElementById("lbCaption");
const lbCounter = document.getElementById("lbCounter");
if (!img || !lbImg) return;
lbImg.src = img.src;
lbImg.alt = img.caption;
lbImg.loading = "eager";
if (lbCaption) lbCaption.textContent = img.caption;
if (lbCounter) lbCounter.textContent = `${lightboxIndex + 1} / ${visibleImages.length}`;
}
function closeLightbox() {
const lb = document.getElementById("lightbox");
if (lb) lb.hidden = true;
}
function init() {
const lb = document.getElementById("lightbox");
const lbClose = document.getElementById("lbClose");
const lbPrev = document.getElementById("lbPrev");
const lbNext = document.getElementById("lbNext");
const galleryFilters = document.getElementById("galleryFilters");
if (lbClose) {
lbClose.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
closeLightbox();
});
}
if (lb) {
lb.addEventListener("click", (e) => {
if (e.target === lb) closeLightbox();
});
}
if (lbPrev) {
lbPrev.addEventListener("click", () => {
lightboxIndex = (lightboxIndex - 1 + visibleImages.length) % visibleImages.length;
updateLightbox();
});
}
if (lbNext) {
lbNext.addEventListener("click", () => {
lightboxIndex = (lightboxIndex + 1) % visibleImages.length;
updateLightbox();
});
}
document.addEventListener("keydown", (e) => {
if (lb?.hidden) return;
if (e.key === "Escape") closeLightbox();
if (e.key === "ArrowLeft") {
lightboxIndex = (lightboxIndex - 1 + visibleImages.length) % visibleImages.length;
updateLightbox();
}
if (e.key === "ArrowRight") {
lightboxIndex = (lightboxIndex + 1) % visibleImages.length;
updateLightbox();
}
});
if (galleryFilters) {
galleryFilters.addEventListener("click", (e) => {
const btn = e.target.closest(".gf-btn");
if (!btn) return;
document.querySelectorAll(".gf-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
activeFilter = btn.dataset.filter;
buildGrid();
});
}
buildGrid();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Image Gallery Grid</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<div class="gallery-toolbar">
<div class="gallery-filters" id="galleryFilters">
<button class="gf-btn active" data-filter="all">All</button>
<button class="gf-btn" data-filter="nature">Nature</button>
<button class="gf-btn" data-filter="architecture">Architecture</button>
<button class="gf-btn" data-filter="abstract">Abstract</button>
</div>
<span class="gallery-count" id="galleryCount"></span>
</div>
<div class="gallery-grid" id="galleryGrid"></div>
</div>
<!-- Lightbox -->
<div class="lightbox" id="lightbox" hidden>
<button type="button" class="lb-close" id="lbClose" aria-label="Close">✕</button>
<button type="button" class="lb-prev" id="lbPrev" aria-label="Previous">‹</button>
<button type="button" class="lb-next" id="lbNext" aria-label="Next">›</button>
<div class="lb-img-wrap">
<img class="lb-img" id="lbImg" src="" alt="" />
</div>
<div class="lb-caption" id="lbCaption"></div>
<div class="lb-counter" id="lbCounter"></div>
</div>
<script src="script.js"></script>
</body>
</html>Masonry photo gallery using CSS columns with hover zoom effect and overlay. Clicking any image opens a full-screen lightbox with previous/next buttons, keyboard (←/→/Esc) navigation, and touch swipe.