New HTML Gallery (IN PROGESS) - Tabs for keyboard accessibility, but has two tabs it shouldnt.
See it Here: https://izzysworld.specialdistrict.org/amplify-homepage-preview
How to use:
add it to an HTML element. Add photos as normal images to another page not linked anywhere. Take the URL and replace the bolded URL below.
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Accessible Image Gallery with Keyboard Support</title>
<style>
/* Visually hidden text for screen readers */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
.gallery-container * { box-sizing: border-box; margin: 0; padding: 0; }
.gallery-container { background: #fff; padding: 20px; font-family: Arial, sans-serif; }
.gallery-container .gallery { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
.gallery-container .gallery-item { position: relative; width: calc(25% - 10px); height: 250px; overflow: hidden; cursor: pointer; }
.gallery-container .gallery-item img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s ease; }
.gallery-container .gallery-item:hover img { transform: scale(1.1); }
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.8); }
#modalContent { margin: 5% auto; text-align: center; }
#modalContent img { max-width: 90%; max-height: 80%; display: block; margin: auto; outline: none; }
.close-button, .nav-button { position: absolute; background: transparent; border: none; color: #fff; cursor: pointer; }
.close-button { top: 20px; right: 30px; font-size: 30px; }
.nav-button { top: 50%; transform: translateY(-50%); font-size: 40px; padding: 10px; }
.nav-left { left: 20px; }
.nav-right { right: 20px; }
.close-button:focus, .nav-button:focus, #modalContent img:focus { outline: 2px solid #fff; }
</style>
</head>
<body>
<div class="gallery-container">
<div class="gallery" id="gallery"></div>
</div>
<div id="modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<h2 id="modalTitle" class="visually-hidden">Image preview</h2>
<button class="close-button" id="closeModal" aria-label="Close modal">×</button>
<button class="nav-button nav-left" id="prevBtn" aria-label="Previous image">❮</button>
<button class="nav-button nav-right" id="nextBtn" aria-label="Next image">❯</button>
<div id="modalContent" tabindex="0"></div>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
let lastFocused = null;
let images = [];
let currentIndex = 0;
const modal = document.getElementById("modal");
const content = document.getElementById("modalContent");
function trapFocus(e, first, last) {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault(); last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault(); first.focus();
}
}
}
function openModal(idx) {
lastFocused = document.activeElement;
currentIndex = idx;
updateModal();
modal.style.display = 'block';
const focusable = [
document.getElementById('closeModal'),
document.getElementById('prevBtn'),
document.getElementById('nextBtn'),
content.querySelector('img')
].filter(Boolean);
const first = focusable[0];
const last = focusable[focusable.length - 1];
first.focus();
modal.addEventListener('keydown', e => trapFocus(e, first, last));
modal.addEventListener('keydown', modalKeyHandler);
}
function modalKeyHandler(e) {
if (e.key === 'Escape') closeModal();
if (e.key === 'ArrowLeft') showPrev();
if (e.key === 'ArrowRight') showNext();
}
function updateModal() {
const item = images[currentIndex];
content.innerHTML = `<img src="${item.src}" alt="${item.alt}" tabindex="0">`;
}
function closeModal() {
modal.style.display = 'none';
modal.removeEventListener('keydown', modalKeyHandler);
lastFocused && lastFocused.focus();
}
function showPrev() {
currentIndex = (currentIndex - 1 + images.length) % images.length;
updateModal();
}
function showNext() {
currentIndex = (currentIndex + 1) % images.length;
updateModal();
}
document.getElementById('closeModal').addEventListener('click', closeModal);
modal.addEventListener('click', e => { if (e.target === modal) closeModal(); });
document.getElementById('prevBtn').addEventListener('click', showPrev);
document.getElementById('nextBtn').addEventListener('click', showNext);
function loadGallery() {
fetch('https://izzysworld.specialdistrict.org/bucketlist-gallery-attempth')
.then(r => r.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const figs = doc.querySelectorAll('figure');
images = Array.from(figs).map(f => {
const img = f.querySelector('img');
return img ? { src: img.src, alt: img.alt || (f.querySelector('figcaption')?.innerText) || 'Image' } : null;
}).filter(Boolean);
const gallery = document.getElementById('gallery');
gallery.innerHTML = '';
images.forEach((imgData, i) => {
const div = document.createElement('div');
div.className = 'gallery-item';
div.tabIndex = 0;
const img = document.createElement('img');
img.src = imgData.src;
img.alt = imgData.alt;
div.appendChild(img);
div.addEventListener('click', () => openModal(i));
div.addEventListener('keydown', e => { if (e.key === 'Enter') openModal(i); });
gallery.appendChild(div);
});
})
.catch(err => console.error('Gallery load error:', err));
}
loadGallery();
});
</script>
</body>
</html>