Content & Media

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.

Live demo

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.

Hosted (recommended)html
<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 -->
Inline (self-contained)html
<!-- 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.

AttributeDefaultDescription
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

Need 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