I’ll build this as a standalone advanced gallery chunk for your RX Theme path, using vanilla JavaScript so it stays fast and theme-friendly. It will include lightbox, keyboard control, lazy loading, filtering, masonry helper, captions, accessibility, and safe auto-init.
Yes friend. Create this file:
assets/static-js/static-chunks-js/chunk-040-gallery.js
Then paste the full code below.
/*!
* RX Theme Gallery Chunk
* File: assets/static-js/static-chunks-js/chunk-040-gallery.js
* Purpose: Advanced gallery, lightbox, filter, masonry helper, lazy image support
* Author: RX Theme
*/
(function () {
'use strict';
/**
* RX Gallery Chunk
*
* Supported HTML examples:
*
* <div class="rx-gallery" data-rx-gallery>
* <a href="large.jpg" class="rx-gallery__item" data-caption="Image caption">
* <img src="thumb.jpg" alt="Example image">
* </a>
* </div>
*
* Optional filters:
*
* <div class="rx-gallery-filter" data-rx-gallery-filter>
* <button data-filter="all">All</button>
* <button data-filter="bone">Bone</button>
* <button data-filter="eye">Eye</button>
* </div>
*
* <a class="rx-gallery__item" data-category="bone">...</a>
*/
const RXGallery = {
config: {
gallerySelector: '[data-rx-gallery], .rx-gallery',
itemSelector: '.rx-gallery__item, [data-rx-gallery-item]',
filterSelector: '[data-rx-gallery-filter]',
filterButtonSelector: '[data-filter]',
activeClass: 'is-active',
hiddenClass: 'is-hidden',
loadedClass: 'is-loaded',
lightboxOpenClass: 'rx-lightbox-open',
transitionDuration: 250,
enableKeyboard: true,
enableSwipe: true,
enableHash: false,
enableMasonry: true,
enableLazyLoading: true,
closeOnOverlayClick: true,
closeOnEscape: true,
preloadNextImage: true
},
state: {
galleries: [],
currentGallery: null,
currentItems: [],
currentIndex: 0,
lightbox: null,
isOpen: false,
lastFocusedElement: null,
touchStartX: 0,
touchEndX: 0
},
init() {
this.collectGalleries();
this.setupLazyImages();
this.setupFilters();
this.setupGalleryItems();
this.setupMasonry();
this.setupGlobalEvents();
this.initFromHash();
document.documentElement.classList.add('rx-gallery-ready');
},
collectGalleries() {
const galleries = document.querySelectorAll(this.config.gallerySelector);
this.state.galleries = Array.from(galleries).filter((gallery) => {
return !gallery.dataset.rxGalleryInitialized;
});
this.state.galleries.forEach((gallery, index) => {
gallery.dataset.rxGalleryInitialized = 'true';
gallery.dataset.rxGalleryId = gallery.dataset.rxGalleryId || `rx-gallery-${index + 1}`;
gallery.setAttribute('role', 'list');
const items = gallery.querySelectorAll(this.config.itemSelector);
items.forEach((item, itemIndex) => {
item.dataset.rxGalleryIndex = String(itemIndex);
item.setAttribute('role', 'listitem');
if (!item.hasAttribute('aria-label')) {
item.setAttribute('aria-label', `Open gallery image ${itemIndex + 1}`);
}
});
});
},
setupGalleryItems() {
this.state.galleries.forEach((gallery) => {
const items = gallery.querySelectorAll(this.config.itemSelector);
items.forEach((item) => {
item.addEventListener('click', (event) => {
const href = item.getAttribute('href');
const imageSrc = this.getItemImageSrc(item);
if (!href && !imageSrc) return;
event.preventDefault();
const visibleItems = this.getVisibleItems(gallery);
const index = visibleItems.indexOf(item);
this.openLightbox(gallery, visibleItems, index >= 0 ? index : 0);
});
});
});
},
getVisibleItems(gallery) {
const items = Array.from(gallery.querySelectorAll(this.config.itemSelector));
return items.filter((item) => {
return !item.classList.contains(this.config.hiddenClass) && item.offsetParent !== null;
});
},
getItemImageSrc(item) {
const href = item.getAttribute('href');
const dataFull = item.dataset.full;
const dataSrc = item.dataset.src;
const image = item.querySelector('img');
const imageSrc = image ? image.currentSrc || image.src || image.dataset.src : '';
return dataFull || href || dataSrc || imageSrc || '';
},
getItemCaption(item) {
const caption =
item.dataset.caption ||
item.getAttribute('title') ||
item.querySelector('figcaption')?.textContent ||
item.querySelector('img')?.getAttribute('alt') ||
'';
return caption.trim();
},
getItemAlt(item) {
const image = item.querySelector('img');
const alt = image ? image.getAttribute('alt') : '';
return alt || this.getItemCaption(item) || 'Gallery image';
},
createLightbox() {
if (this.state.lightbox) return this.state.lightbox;
const lightbox = document.createElement('div');
lightbox.className = 'rx-lightbox';
lightbox.setAttribute('role', 'dialog');
lightbox.setAttribute('aria-modal', 'true');
lightbox.setAttribute('aria-label', 'Image gallery lightbox');
lightbox.innerHTML = `
<div class="rx-lightbox__overlay" data-rx-lightbox-close></div>
<div class="rx-lightbox__dialog" role="document">
<button class="rx-lightbox__close" type="button" aria-label="Close gallery" data-rx-lightbox-close>
<span aria-hidden="true">×</span>
</button>
<button class="rx-lightbox__nav rx-lightbox__nav--prev" type="button" aria-label="Previous image" data-rx-lightbox-prev>
<span aria-hidden="true">❮</span>
</button>
<figure class="rx-lightbox__figure">
<div class="rx-lightbox__loader" aria-hidden="true"></div>
<img class="rx-lightbox__image" src="" alt="">
<figcaption class="rx-lightbox__caption"></figcaption>
</figure>
<button class="rx-lightbox__nav rx-lightbox__nav--next" type="button" aria-label="Next image" data-rx-lightbox-next>
<span aria-hidden="true">❯</span>
</button>
<div class="rx-lightbox__counter" aria-live="polite"></div>
</div>
`;
document.body.appendChild(lightbox);
lightbox.querySelectorAll('[data-rx-lightbox-close]').forEach((button) => {
button.addEventListener('click', (event) => {
if (
event.target.matches('.rx-lightbox__overlay') &&
!this.config.closeOnOverlayClick
) {
return;
}
this.closeLightbox();
});
});
lightbox.querySelector('[data-rx-lightbox-prev]').addEventListener('click', () => {
this.prevImage();
});
lightbox.querySelector('[data-rx-lightbox-next]').addEventListener('click', () => {
this.nextImage();
});
if (this.config.enableSwipe) {
lightbox.addEventListener(
'touchstart',
(event) => {
this.state.touchStartX = event.changedTouches[0].screenX;
},
{ passive: true }
);
lightbox.addEventListener(
'touchend',
(event) => {
this.state.touchEndX = event.changedTouches[0].screenX;
this.handleSwipe();
},
{ passive: true }
);
}
this.state.lightbox = lightbox;
return lightbox;
},
openLightbox(gallery, items, index) {
if (!items.length) return;
const lightbox = this.createLightbox();
this.state.currentGallery = gallery;
this.state.currentItems = items;
this.state.currentIndex = index;
this.state.isOpen = true;
this.state.lastFocusedElement = document.activeElement;
document.body.classList.add(this.config.lightboxOpenClass);
lightbox.classList.add('is-open');
lightbox.removeAttribute('hidden');
this.loadCurrentImage();
this.trapFocus();
const closeButton = lightbox.querySelector('.rx-lightbox__close');
if (closeButton) closeButton.focus();
if (this.config.enableHash) {
const galleryId = gallery.dataset.rxGalleryId || 'gallery';
window.history.replaceState(null, '', `#${galleryId}-${index + 1}`);
}
},
closeLightbox() {
const lightbox = this.state.lightbox;
if (!lightbox) return;
this.state.isOpen = false;
document.body.classList.remove(this.config.lightboxOpenClass);
lightbox.classList.remove('is-open');
if (this.state.lastFocusedElement && this.state.lastFocusedElement.focus) {
this.state.lastFocusedElement.focus();
}
if (this.config.enableHash && window.location.hash) {
window.history.replaceState(null, '', window.location.pathname + window.location.search);
}
},
loadCurrentImage() {
const lightbox = this.state.lightbox;
const item = this.state.currentItems[this.state.currentIndex];
if (!lightbox || !item) return;
const image = lightbox.querySelector('.rx-lightbox__image');
const caption = lightbox.querySelector('.rx-lightbox__caption');
const counter = lightbox.querySelector('.rx-lightbox__counter');
const loader = lightbox.querySelector('.rx-lightbox__loader');
const prevButton = lightbox.querySelector('[data-rx-lightbox-prev]');
const nextButton = lightbox.querySelector('[data-rx-lightbox-next]');
const src = this.getItemImageSrc(item);
const alt = this.getItemAlt(item);
const captionText = this.getItemCaption(item);
loader.classList.add('is-visible');
image.classList.remove(this.config.loadedClass);
image.onload = () => {
loader.classList.remove('is-visible');
image.classList.add(this.config.loadedClass);
};
image.onerror = () => {
loader.classList.remove('is-visible');
caption.textContent = 'Image could not be loaded.';
};
image.src = src;
image.alt = alt;
caption.textContent = captionText;
caption.style.display = captionText ? '' : 'none';
counter.textContent = `${this.state.currentIndex + 1} / ${this.state.currentItems.length}`;
const hasMultiple = this.state.currentItems.length > 1;
prevButton.style.display = hasMultiple ? '' : 'none';
nextButton.style.display = hasMultiple ? '' : 'none';
this.markActiveItem();
if (this.config.preloadNextImage) {
this.preloadAroundCurrent();
}
},
markActiveItem() {
this.state.currentItems.forEach((item, index) => {
item.classList.toggle(this.config.activeClass, index === this.state.currentIndex);
});
},
nextImage() {
if (!this.state.currentItems.length) return;
this.state.currentIndex =
(this.state.currentIndex + 1) % this.state.currentItems.length;
this.loadCurrentImage();
},
prevImage() {
if (!this.state.currentItems.length) return;
this.state.currentIndex =
(this.state.currentIndex - 1 + this.state.currentItems.length) %
this.state.currentItems.length;
this.loadCurrentImage();
},
preloadAroundCurrent() {
const nextIndex = (this.state.currentIndex + 1) % this.state.currentItems.length;
const prevIndex =
(this.state.currentIndex - 1 + this.state.currentItems.length) %
this.state.currentItems.length;
[nextIndex, prevIndex].forEach((index) => {
const item = this.state.currentItems[index];
if (!item) return;
const src = this.getItemImageSrc(item);
if (!src) return;
const image = new Image();
image.src = src;
});
},
handleSwipe() {
const distance = this.state.touchEndX - this.state.touchStartX;
const minDistance = 50;
if (Math.abs(distance) < minDistance) return;
if (distance < 0) {
this.nextImage();
} else {
this.prevImage();
}
},
setupGlobalEvents() {
if (this.config.enableKeyboard) {
document.addEventListener('keydown', (event) => {
if (!this.state.isOpen) return;
if (event.key === 'Escape' && this.config.closeOnEscape) {
this.closeLightbox();
}
if (event.key === 'ArrowRight') {
this.nextImage();
}
if (event.key === 'ArrowLeft') {
this.prevImage();
}
if (event.key === 'Tab') {
this.keepFocusInsideLightbox(event);
}
});
}
window.addEventListener('resize', this.debounce(() => {
this.refreshMasonry();
}, 150));
window.addEventListener('load', () => {
this.refreshMasonry();
});
},
trapFocus() {
const lightbox = this.state.lightbox;
if (!lightbox) return;
const focusable = this.getFocusableElements(lightbox);
if (focusable.length) {
focusable[0].focus();
}
},
keepFocusInsideLightbox(event) {
const lightbox = this.state.lightbox;
if (!lightbox) return;
const focusable = this.getFocusableElements(lightbox);
if (!focusable.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
},
getFocusableElements(container) {
return Array.from(
container.querySelectorAll(
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
)
).filter((element) => {
return element.offsetWidth > 0 || element.offsetHeight > 0;
});
},
setupLazyImages() {
if (!this.config.enableLazyLoading) return;
const images = document.querySelectorAll(
'.rx-gallery img[data-src], [data-rx-gallery] img[data-src]'
);
if (!images.length) return;
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(
(entries, instance) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const image = entry.target;
this.loadLazyImage(image);
instance.unobserve(image);
});
},
{
root: null,
rootMargin: '200px 0px',
threshold: 0.01
}
);
images.forEach((image) => observer.observe(image));
} else {
images.forEach((image) => this.loadLazyImage(image));
}
},
loadLazyImage(image) {
const src = image.dataset.src;
const srcset = image.dataset.srcset;
const sizes = image.dataset.sizes;
if (srcset) image.srcset = srcset;
if (sizes) image.sizes = sizes;
if (src) image.src = src;
image.addEventListener(
'load',
() => {
image.classList.add(this.config.loadedClass);
image.removeAttribute('data-src');
image.removeAttribute('data-srcset');
image.removeAttribute('data-sizes');
},
{ once: true }
);
},
setupFilters() {
const filterGroups = document.querySelectorAll(this.config.filterSelector);
filterGroups.forEach((filterGroup) => {
const targetSelector = filterGroup.dataset.target;
const gallery = targetSelector
? document.querySelector(targetSelector)
: this.findNearestGallery(filterGroup);
if (!gallery) return;
const buttons = filterGroup.querySelectorAll(this.config.filterButtonSelector);
buttons.forEach((button) => {
button.addEventListener('click', () => {
const filter = button.dataset.filter || 'all';
buttons.forEach((btn) => {
btn.classList.remove(this.config.activeClass);
btn.setAttribute('aria-pressed', 'false');
});
button.classList.add(this.config.activeClass);
button.setAttribute('aria-pressed', 'true');
this.filterGallery(gallery, filter);
});
});
});
},
findNearestGallery(filterGroup) {
const parent = filterGroup.parentElement;
if (!parent) return null;
return (
parent.querySelector(this.config.gallerySelector) ||
document.querySelector(this.config.gallerySelector)
);
},
filterGallery(gallery, filter) {
const items = gallery.querySelectorAll(this.config.itemSelector);
items.forEach((item) => {
const categories = (item.dataset.category || '')
.split(',')
.map((category) => category.trim().toLowerCase())
.filter(Boolean);
const shouldShow =
filter === 'all' ||
categories.includes(filter.toLowerCase()) ||
item.classList.contains(`category-${filter}`);
item.classList.toggle(this.config.hiddenClass, !shouldShow);
item.setAttribute('aria-hidden', shouldShow ? 'false' : 'true');
});
setTimeout(() => {
this.refreshMasonry(gallery);
}, this.config.transitionDuration);
},
setupMasonry() {
if (!this.config.enableMasonry) return;
this.state.galleries.forEach((gallery) => {
if (
gallery.dataset.layout === 'masonry' ||
gallery.classList.contains('rx-gallery--masonry')
) {
this.applyMasonry(gallery);
}
});
},
refreshMasonry(gallery = null) {
if (!this.config.enableMasonry) return;
if (gallery) {
this.applyMasonry(gallery);
return;
}
this.state.galleries.forEach((itemGallery) => {
if (
itemGallery.dataset.layout === 'masonry' ||
itemGallery.classList.contains('rx-gallery--masonry')
) {
this.applyMasonry(itemGallery);
}
});
},
applyMasonry(gallery) {
const items = Array.from(gallery.querySelectorAll(this.config.itemSelector));
if (!items.length) return;
const computedStyle = window.getComputedStyle(gallery);
const rowGap = parseInt(computedStyle.getPropertyValue('row-gap'), 10) || 16;
items.forEach((item) => {
if (item.classList.contains(this.config.hiddenClass)) {
item.style.gridRowEnd = '';
return;
}
const itemHeight = item.getBoundingClientRect().height;
const rowHeight = parseInt(
computedStyle.getPropertyValue('grid-auto-rows'),
10
) || 10;
const rowSpan = Math.ceil((itemHeight + rowGap) / (rowHeight + rowGap));
item.style.gridRowEnd = `span ${rowSpan}`;
});
},
initFromHash() {
if (!this.config.enableHash || !window.location.hash) return;
const hash = window.location.hash.replace('#', '');
this.state.galleries.forEach((gallery) => {
const galleryId = gallery.dataset.rxGalleryId;
if (!hash.startsWith(galleryId)) return;
const number = parseInt(hash.replace(`${galleryId}-`, ''), 10);
const index = Number.isNaN(number) ? 0 : number - 1;
const items = this.getVisibleItems(gallery);
if (items[index]) {
this.openLightbox(gallery, items, index);
}
});
},
debounce(callback, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
callback.apply(this, args);
}, delay);
};
}
};
function autoInit() {
RXGallery.init();
window.RXGallery = RXGallery;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', autoInit);
} else {
autoInit();
}
})();
Add this CSS also, otherwise the lightbox and masonry will work but not look perfect.
/* RX Gallery Basic Style */
.rx-gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--rx-gallery-gap, 16px);
}
.rx-gallery--masonry {
grid-auto-rows: 10px;
}
.rx-gallery__item {
position: relative;
display: block;
overflow: hidden;
border-radius: var(--rx-radius-md, 12px);
background: var(--rx-color-surface, #f5f5f5);
cursor: pointer;
}
.rx-gallery__item img {
display: block;
width: 100%;
height: auto;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.rx-gallery__item:hover img {
transform: scale(1.04);
}
.rx-gallery__item.is-hidden {
display: none;
}
.rx-gallery__item.is-active {
outline: 3px solid var(--rx-color-primary, #2563eb);
outline-offset: 3px;
}
.rx-gallery-filter {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 18px;
}
.rx-gallery-filter button {
border: 1px solid var(--rx-color-border, #ddd);
background: var(--rx-color-surface, #fff);
color: var(--rx-color-text, #111);
padding: 8px 14px;
border-radius: 999px;
cursor: pointer;
}
.rx-gallery-filter button.is-active {
background: var(--rx-color-primary, #2563eb);
border-color: var(--rx-color-primary, #2563eb);
color: #fff;
}
/* Lightbox */
.rx-lightbox {
position: fixed;
inset: 0;
z-index: 999999;
display: none;
align-items: center;
justify-content: center;
}
.rx-lightbox.is-open {
display: flex;
}
.rx-lightbox__overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.86);
}
.rx-lightbox__dialog {
position: relative;
z-index: 2;
width: min(96vw, 1200px);
max-height: 94vh;
display: flex;
align-items: center;
justify-content: center;
}
.rx-lightbox__figure {
margin: 0;
max-width: 100%;
max-height: 94vh;
text-align: center;
}
.rx-lightbox__image {
max-width: 100%;
max-height: 82vh;
opacity: 0;
border-radius: 12px;
transition: opacity 0.25s ease;
}
.rx-lightbox__image.is-loaded {
opacity: 1;
}
.rx-lightbox__caption {
margin-top: 12px;
color: #fff;
font-size: 15px;
line-height: 1.6;
}
.rx-lightbox__close,
.rx-lightbox__nav {
position: absolute;
z-index: 3;
border: 0;
background: rgba(255, 255, 255, 0.14);
color: #fff;
cursor: pointer;
transition: background 0.2s ease, transform 0.2s ease;
}
.rx-lightbox__close:hover,
.rx-lightbox__nav:hover {
background: rgba(255, 255, 255, 0.25);
}
.rx-lightbox__close {
top: 12px;
right: 12px;
width: 44px;
height: 44px;
border-radius: 999px;
font-size: 30px;
line-height: 44px;
}
.rx-lightbox__nav {
top: 50%;
width: 48px;
height: 64px;
transform: translateY(-50%);
border-radius: 12px;
font-size: 34px;
}
.rx-lightbox__nav--prev {
left: 12px;
}
.rx-lightbox__nav--next {
right: 12px;
}
.rx-lightbox__counter {
position: absolute;
left: 16px;
bottom: 16px;
color: #fff;
font-size: 14px;
opacity: 0.9;
}
.rx-lightbox__loader {
position: absolute;
top: 50%;
left: 50%;
width: 42px;
height: 42px;
margin-top: -21px;
margin-left: -21px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 999px;
display: none;
animation: rxLightboxSpin 0.8s linear infinite;
}
.rx-lightbox__loader.is-visible {
display: block;
}
.rx-lightbox-open {
overflow: hidden;
}
@keyframes rxLightboxSpin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 768px) {
.rx-lightbox__nav {
width: 42px;
height: 52px;
font-size: 28px;
}
.rx-lightbox__nav--prev {
left: 6px;
}
.rx-lightbox__nav--next {
right: 6px;
}
.rx-lightbox__caption {
font-size: 14px;
padding: 0 12px;
}
}
Use this HTML example:
<div class="rx-gallery-filter" data-rx-gallery-filter>
<button type="button" data-filter="all" class="is-active">All</button>
<button type="button" data-filter="orthopedic">Orthopedic</button>
<button type="button" data-filter="eye">Eye</button>
</div>
<div class="rx-gallery rx-gallery--masonry" data-rx-gallery data-layout="masonry">
<a
href="large-image-1.jpg"
class="rx-gallery__item"
data-category="orthopedic"
data-caption="Orthopedic medical image"
>
<img src="thumb-image-1.jpg" alt="Orthopedic image">
</a>
<a
href="large-image-2.jpg"
class="rx-gallery__item"
data-category="eye"
data-caption="Eye disease medical image"
>
<img src="thumb-image-2.jpg" alt="Eye disease image">
</a>
</div>
Then enqueue it in RX Theme:
wp_enqueue_script(
'rx-chunk-gallery',
get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-040-gallery.js',
array(),
wp_get_theme()->get('Version'),
true
);
Best file name for your system is exactly:
chunk-040-gallery.js
This keeps your RX Theme organized as an advanced static chunk system.