Responsive Image Gallery with CSS Grid and Modal
Create a responsive image gallery that opens images in a modal view with a smooth transition.
Code
<!--
Responsive Image Gallery + Modal (CSS Grid)
Usage: Add images with data-modal="true" inside .gallery; click to open in an accessible modal.
-->
<div class="gallery" aria-label="Image gallery">
<img src="image1.jpg" alt="Image 1" data-modal="true" loading="lazy" decoding="async">
<img src="image2.jpg" alt="Image 2" data-modal="true" loading="lazy" decoding="async">
<img src="image3.jpg" alt="Image 3" data-modal="true" loading="lazy" decoding="async">
<div class="modal" id="imageModal" role="dialog" aria-modal="true" aria-hidden="true" aria-label="Image preview">
<button class="close" type="button" aria-label="Close modal">×</button>
<img class="modal-content" id="modalImg" alt="">
</div>
</div>
<style>
:root { color-scheme: light dark; }
.gallery { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px; }
.gallery img { width:100%; aspect-ratio:1/1; object-fit:cover; cursor:pointer; border-radius:10px;
transition:transform .2s ease, filter .2s ease; will-change:transform; }
.gallery img:hover { transform:scale(1.03); filter:saturate(1.05); }
.gallery img:focus-visible { outline:3px solid CanvasText; outline-offset:3px; }
.modal { position:fixed; inset:0; z-index:1000; display:flex; align-items:center; justify-content:center;
padding:24px; background:rgba(0,0,0,.82); opacity:0; visibility:hidden; transition:opacity .18s ease, visibility .18s ease; }
.modal.is-open { opacity:1; visibility:visible; }
.modal-content { max-width:min(1100px,92vw); max-height:82vh; border-radius:14px; box-shadow:0 16px 60px rgba(0,0,0,.45);
transform:translateY(8px) scale(.98); transition:transform .18s ease; }
.modal.is-open .modal-content { transform:translateY(0) scale(1); }
.close { position:absolute; top:14px; right:14px; width:44px; height:44px; border:0; border-radius:999px;
background:rgba(255,255,255,.12); color:#fff; font-size:30px; line-height:44px; cursor:pointer; }
.close:hover { background:rgba(255,255,255,.18); }
.close:focus-visible { outline:3px solid #fff; outline-offset:3px; }
@media (prefers-reduced-motion: reduce) {
.gallery img, .modal, .modal-content { transition:none !important; }
}
</style>
<script>
// Modal behavior: event delegation (performance), ESC/backdrop close, focus restore, and safe image swapping.
(() => {
const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImg');
const closeBtn = modal.querySelector('.close');
const gallery = document.querySelector('.gallery');
if (!modal || !modalImg || !closeBtn || !gallery) return;
let lastActiveEl = null;
const openModal = (img) => {
lastActiveEl = document.activeElement;
modalImg.alt = img.alt || 'Selected image';
modalImg.src = img.currentSrc || img.src; // supports responsive images if used later
modal.classList.add('is-open');
modal.setAttribute('aria-hidden', 'false');
closeBtn.focus(); // basic focus management for accessibility
document.documentElement.style.overflow = 'hidden'; // prevent background scroll
};
const closeModal = () => {
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden', 'true');
modalImg.removeAttribute('src'); // free memory for large images
document.documentElement.style.overflow = '';
lastActiveEl?.focus?.();
};
gallery.addEventListener('click', (e) => {
const img = e.target.closest('img[data-modal="true"]');
if (img) openModal(img);
});
closeBtn.addEventListener('click', closeModal);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal(); // close on backdrop click only
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.classList.contains('is-open')) closeModal();
});
})();
// Example usage: Add more <img data-modal="true"> inside .gallery.
</script>