Free Image Gallery Lightbox Widget
Responsive thumbnail grid with a full-screen lightbox and keyboard navigation.
A free image gallery widget that turns a simple JSON list of images into a clean thumbnail grid with a full-screen lightbox. It is built for photographers, portfolios, product pages, and blogs that need a gallery without heavy plugins. Paste one script tag with your image data and the gallery appears exactly where you place it.
Embed this widget
Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.
<script src="https://hayneslab.dev/widgets/image-gallery-lightbox.js" data-hl-config='[{"src":"https://placehold.co/800x600/f97316/ffffff?text=Photo+1","alt":"Studio workspace with warm lighting"},{"src":"https://placehold.co/800x600/06b6d4/ffffff?text=Photo+2","alt":"Product flat lay on a cyan backdrop"},{"src":"https://placehold.co/800x600/111827/ffffff?text=Photo+3","alt":"Team collaborating around a whiteboard"},{"src":"https://placehold.co/800x600/4b5563/ffffff?text=Photo+4","alt":"Detail shot of a finished project"},{"src":"https://placehold.co/800x600/0ea5e9/ffffff?text=Photo+5","alt":"Behind the scenes on a client shoot"},{"src":"https://placehold.co/800x600/f59e0b/ffffff?text=Photo+6","alt":"Final deliverables arranged for handoff"}]' async></script>
<!-- Free Image Gallery Lightbox widget by haynes lab — https://hayneslab.dev/tools/widgets/image-gallery-lightbox --><!-- Free Image Gallery Lightbox widget by haynes lab — https://hayneslab.dev/tools/widgets/image-gallery-lightbox -->
<script data-hl-config='[{"src":"https://placehold.co/800x600/f97316/ffffff?text=Photo+1","alt":"Studio workspace with warm lighting"},{"src":"https://placehold.co/800x600/06b6d4/ffffff?text=Photo+2","alt":"Product flat lay on a cyan backdrop"},{"src":"https://placehold.co/800x600/111827/ffffff?text=Photo+3","alt":"Team collaborating around a whiteboard"},{"src":"https://placehold.co/800x600/4b5563/ffffff?text=Photo+4","alt":"Detail shot of a finished project"},{"src":"https://placehold.co/800x600/0ea5e9/ffffff?text=Photo+5","alt":"Behind the scenes on a client shoot"},{"src":"https://placehold.co/800x600/f59e0b/ffffff?text=Photo+6","alt":"Final deliverables arranged for handoff"}]'>
/*!
* haynes lab — Image Gallery Lightbox widget (free)
* Docs & embed code: https://hayneslab.dev/tools/widgets/image-gallery-lightbox
*/
(function () {
'use strict';
var PREFIX = 'hlw-image-gallery-lightbox';
var STYLE_ID = 'hlw-image-gallery-lightbox-styles';
function findScript() {
return document.currentScript ||
document.querySelector('script[src*="widgets/image-gallery-lightbox.js"]');
}
function injectStyles() {
if (document.getElementById(STYLE_ID)) return;
var css =
'.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
'.' + PREFIX + ' { max-width: 960px; margin: 0 auto; padding-bottom: 22px; position: relative;' +
' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
' font-size: 16px; line-height: 1.5; color: #1f2937; }' +
'.' + PREFIX + '__grid { display: grid; grid-template-columns: repeat(var(--hlw-cols, 3), 1fr); gap: 12px; }' +
'.' + PREFIX + '__thumb { display: block; width: 100%; padding: 0; border: 1px solid #e5e7eb;' +
' border-radius: 8px; overflow: hidden; background: #f3f4f6; cursor: zoom-in;' +
' transition: transform 0.2s ease, box-shadow 0.2s ease; }' +
'.' + PREFIX + '__thumb:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0,0,0,0.12); }' +
'.' + PREFIX + '__thumb:focus-visible { outline: 2px solid #f97316; outline-offset: 2px; }' +
'.' + PREFIX + '__thumb img { display: block; width: 100%; height: 100%; aspect-ratio: 4 / 3; object-fit: cover; }' +
'.' + PREFIX + '__credit { position: absolute; right: 2px; bottom: 0; font-size: 11px;' +
' display: inline-flex; align-items: center; gap: 4px; color: #9ca3af; text-decoration: none; }' +
'.' + PREFIX + '__credit svg { display: block; }' +
'.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }' +
'.' + PREFIX + '__overlay { position: fixed; inset: 0; z-index: 999999; background: rgba(15, 10, 10, 0.92);' +
' display: flex; align-items: center; justify-content: center; padding: 48px 64px; }' +
'.' + PREFIX + '__figure { max-width: 100%; max-height: 100%; margin: 0; text-align: center; }' +
'.' + PREFIX + '__image { max-width: 100%; max-height: calc(100vh - 160px); border-radius: 8px;' +
' box-shadow: 0 20px 60px rgba(0,0,0,0.5); }' +
'.' + PREFIX + '__caption { margin-top: 12px; color: #d1d5db; font-size: 14px; }' +
'.' + PREFIX + '__counter { position: absolute; top: 16px; left: 20px; color: #9ca3af; font-size: 13px; }' +
'.' + PREFIX + '__btn { position: absolute; width: 42px; height: 42px; border: 0; border-radius: 50%;' +
' background: rgba(255,255,255,0.1); color: #ffffff; font-size: 22px; line-height: 1; cursor: pointer;' +
' display: flex; align-items: center; justify-content: center; padding: 0;' +
' transition: background 0.15s ease; }' +
'.' + PREFIX + '__btn:hover { background: rgba(255,255,255,0.25); }' +
'.' + PREFIX + '__btn:focus-visible { outline: 2px solid #f97316; outline-offset: 2px; }' +
'.' + PREFIX + '__btn--close { top: 14px; right: 16px; font-size: 18px; }' +
'.' + PREFIX + '__btn--prev { left: 16px; top: 50%; transform: translateY(-50%); }' +
'.' + PREFIX + '__btn--next { right: 16px; top: 50%; transform: translateY(-50%); }' +
'@media (max-width: 560px) {' +
' .' + PREFIX + '__grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }' +
' .' + PREFIX + '__overlay { padding: 56px 8px 24px; }' +
' .' + PREFIX + '__btn--prev { left: 8px; }' +
' .' + PREFIX + '__btn--next { right: 8px; }' +
' .' + PREFIX + '__image { max-height: calc(100vh - 200px); } }';
var style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = css;
document.head.appendChild(style);
}
function parseImages(script) {
var raw = script.getAttribute('data-hl-config');
if (!raw) {
console.warn('[haynes lab image-gallery-lightbox] Missing required data-hl-config attribute (JSON array of {src, alt}). Nothing rendered.');
return null;
}
var items;
try {
items = JSON.parse(raw);
} catch (e) {
console.warn('[haynes lab image-gallery-lightbox] data-hl-config is not valid JSON. Nothing rendered.', e);
return null;
}
if (!Array.isArray(items)) {
console.warn('[haynes lab image-gallery-lightbox] data-hl-config must be a JSON array. Nothing rendered.');
return null;
}
var valid = items.filter(function (item) {
return item && typeof item.src === 'string' && item.src;
});
if (!valid.length) {
console.warn('[haynes lab image-gallery-lightbox] data-hl-config has no valid images (each needs at least "src"). Nothing rendered.');
return null;
}
return valid;
}
function el(tag, className, text) {
var node = document.createElement(tag);
if (className) node.className = className;
if (text != null) node.textContent = text;
return node;
}
function logoSvg() {
var svgNS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('viewBox', '0 0 32 32');
svg.setAttribute('width', '13');
svg.setAttribute('height', '13');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('focusable', 'false');
function circle(cx, cy, r, attrs) {
var c = document.createElementNS(svgNS, 'circle');
c.setAttribute('cx', cx);
c.setAttribute('cy', cy);
c.setAttribute('r', r);
for (var key in attrs) c.setAttribute(key, attrs[key]);
svg.appendChild(c);
}
circle(16, 16, 16, { fill: '#0f0a0a' });
circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
circle(16, 16, 3, { fill: '#06b6d4' });
circle(24.5, 7.5, 2, { fill: '#f97316' });
circle(7, 16, 1.8, { fill: '#f97316' });
circle(20, 20, 1.5, { fill: '#f97316' });
return svg;
}
function build(script) {
var items = parseImages(script);
if (!items) return;
var columns = parseInt(script.getAttribute('data-hl-columns'), 10);
if (isNaN(columns) || columns < 1 || columns > 6) columns = 3;
injectStyles();
var root = el('div', PREFIX);
root.style.setProperty('--hlw-cols', String(columns));
var grid = el('div', PREFIX + '__grid');
root.appendChild(grid);
var overlay = null;
var imageEl = null;
var captionEl = null;
var counterEl = null;
var index = 0;
var lastTrigger = null;
function render() {
var item = items[index];
imageEl.src = item.src;
imageEl.alt = typeof item.alt === 'string' ? item.alt : '';
captionEl.textContent = typeof item.alt === 'string' ? item.alt : '';
counterEl.textContent = (index + 1) + ' / ' + items.length;
}
function go(i) {
index = (i + items.length) % items.length;
render();
}
function onKeydown(e) {
if (e.key === 'Escape') { e.preventDefault(); close(); }
else if (e.key === 'ArrowLeft') { e.preventDefault(); go(index - 1); }
else if (e.key === 'ArrowRight') { e.preventDefault(); go(index + 1); }
}
function close() {
if (!overlay) return;
document.removeEventListener('keydown', onKeydown);
overlay.parentNode.removeChild(overlay);
overlay = null;
document.body.style.overflow = '';
if (lastTrigger && lastTrigger.focus) lastTrigger.focus();
}
function open(i, trigger) {
if (overlay) return;
index = i;
lastTrigger = trigger;
overlay = el('div', PREFIX + '__overlay');
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-label', 'Image lightbox');
var figure = el('figure', PREFIX + '__figure');
imageEl = el('img', PREFIX + '__image');
captionEl = el('figcaption', PREFIX + '__caption');
figure.appendChild(imageEl);
figure.appendChild(captionEl);
overlay.appendChild(figure);
counterEl = el('div', PREFIX + '__counter');
overlay.appendChild(counterEl);
var closeBtn = el('button', PREFIX + '__btn ' + PREFIX + '__btn--close', '✕');
closeBtn.type = 'button';
closeBtn.setAttribute('aria-label', 'Close lightbox');
closeBtn.addEventListener('click', close);
overlay.appendChild(closeBtn);
if (items.length > 1) {
var prev = el('button', PREFIX + '__btn ' + PREFIX + '__btn--prev', '‹');
prev.type = 'button';
prev.setAttribute('aria-label', 'Previous image');
prev.addEventListener('click', function () { go(index - 1); });
overlay.appendChild(prev);
var next = el('button', PREFIX + '__btn ' + PREFIX + '__btn--next', '›');
next.type = 'button';
next.setAttribute('aria-label', 'Next image');
next.addEventListener('click', function () { go(index + 1); });
overlay.appendChild(next);
}
overlay.addEventListener('click', function (e) {
if (e.target === overlay) close();
});
render();
document.body.style.overflow = 'hidden';
document.body.appendChild(overlay);
document.addEventListener('keydown', onKeydown);
closeBtn.focus();
}
items.forEach(function (item, i) {
var thumb = el('button', PREFIX + '__thumb');
thumb.type = 'button';
thumb.setAttribute('aria-label', 'Open image: ' + (typeof item.alt === 'string' && item.alt ? item.alt : 'image ' + (i + 1)));
var img = document.createElement('img');
img.src = item.src;
img.alt = typeof item.alt === 'string' ? item.alt : '';
img.loading = 'lazy';
thumb.appendChild(img);
thumb.addEventListener('click', function () { open(i, thumb); });
grid.appendChild(thumb);
});
var credit = el('a', PREFIX + '__credit');
credit.appendChild(logoSvg());
credit.appendChild(document.createTextNode('Powered by haynes lab'));
credit.href = 'https://hayneslab.dev/tools/widgets/image-gallery-lightbox';
credit.target = '_blank';
credit.rel = 'noopener';
root.appendChild(credit);
if (script.parentNode && script.parentNode.nodeName !== 'HEAD') {
script.insertAdjacentElement('afterend', root);
} else {
document.body.appendChild(root);
}
}
function init() {
var script = findScript();
if (!script) {
console.warn('[haynes lab image-gallery-lightbox] Could not locate its own script tag. Nothing rendered.');
return;
}
build(script);
}
if (document.body) {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();
</script>Configuration
Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.
| Attribute | Default | Description |
|---|---|---|
data-hl-config | (required) | JSON array of images, each an object with "src" (image URL) and "alt" (alt text, used as the lightbox caption). |
data-hl-columns | "3" | Number of thumbnail columns on desktop (1-6); collapses to 2 columns on small screens. |
Frequently asked questions
How do I install the image gallery lightbox?
Paste the embed snippet where you want the gallery to appear — the thumbnail grid renders inline right after the script tag, and clicking any thumbnail opens the lightbox.
How do I customize the gallery?
Set your images as a JSON array in data-hl-config (each with "src" and "alt"), and change the desktop thumbnail layout with the data-hl-columns attribute.
Does it work on WordPress, Shopify, or Squarespace?
Yes — it is a single vanilla JS file with no dependencies, so it works on any platform that lets you add a custom HTML or code block.
More free widgets
Image Carousel
Responsive image slideshow with arrows, dots, autoplay, and swipe support.
View toolContent & MediaVideo Popup
Clickable thumbnail that opens a YouTube or Vimeo video in a modal.
View toolMarketing & SocialBefore After Slider
Compare two images with a draggable before/after divider.
View toolNeed a custom widget?
We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.
Get in Touch