GalleryFree

Basic Lightbox

Minimal viewer: a clicked thumbnail opens large in a real dialog over a blurred black background; ✕, a click beside it or Escape closes it.

JSModalFade

Updated

Hover or click the scene to interact

This effect is free — the Gallery category contains 44 effects total, including 6 free. Effect.Labs has 840 vanilla effects. Explore the category →

Usage examples

Halle Arvendel

Getting to the venue

12 Tanners' Quay · doors open at 7:30 pm

  • MLine 2, Halle stop — 3 min walk
  • BBuses 14 and 31, Pont-Neuf stop
  • PQuay car park, 420 spaces, open until 1 am

Access map — tap to enlarge

① Access map of a concert hall, sitting next to the transport lines

WhenOn the Halle Arvendel “Getting here” page, the map fits in a small card to the right of the metro and bus stops. A click opens it large to find the exit and the car park.
WhyThe page stays readable with the map shrunk to a thumbnail, and street detail is shown only to whoever asks for it. One thumbnail is enough: the viewer needs no arrows.
SettingsRoot placed in a white card (width: min(40%, 290px)), .blb-thumb at width: 100%, --blb-accent set to the venue red. The “tap to enlarge” line is a plain paragraph inside the root.
Sorbelane School — family areaLina Morvaille, Grade 8

School reports 2025–2026

1st termAverage13.8PDF
2nd termAverage14.6PDF
3rd termAverage15.1PDF

② Reports for the three terms in a school's family area

WhenLina Morvaille has three report cards; the parent clicks the 2nd term preview to read the full page without downloading the PDF.
WhyEach card keeps its average and PDF link beside the preview: the thumbnail button is part of the card, and the viewer is only there to read the document large.
SettingsThe three cards sit inside .blb-grid, turned into a three-column grid; thumbnails take the card's full width and frame the top of the document (object-position: top on the image).

Calvanelle

Today's lunch menu, on your phone at the table: tap a photo to see the dish full screen.

Lunch set €24

Calvanelle
StartersMainsDesserts
Porcini risottomilk · celery
€18
Roast sea bassfish
€22
Apricot tartgluten · egg
€8

③ Photo of a dish in a restaurant menu read on a phone

WhenAt the table, the guest opens Calvanelle's daily menu on their phone and taps the small sea bass photo to see it full screen before ordering.
WhyOn a narrow screen, 58 px photos leave room for prices and allergens; a tap opens the image large and a tap beside it closes it.
SettingsOne row per dish inside .blb-grid (switched to a column), 58 px thumbnail with --blb-radius: 8px. On the effect page the scene switches to the phone alone below 470 px wide.

How it works

Each thumbnail is a real <button class="blb-thumb"> wrapping your <img> (with its alt). On click, tap, or Enter / Space from the keyboard, the script fills the viewer image with the button's data-full attribute when present, otherwise with the thumbnail image itself, then opens the <dialog class="blb-viewer"> with showModal().

The background (rgba(0, 0, 0, 0.9) plus a 10 px blur) fades in while the image grows from 80 % to 100 % of its size in 0.4 s with a slight bounce. The image is shown at its natural size, capped at 80 % of the window width and 70 % of its height.

Being a modal <dialog>, the viewer sits above everything, even when the gallery lives in a transformed or clipped container. All the code is scoped under .fx-0171-root and the blb- prefix; each root is wired on its own, so several galleries share a page and pasting the script twice doubles nothing.

Accessibility

  • Keyboard: thumbnails are buttons, reachable with Tab and activated with Enter or Space. On opening, focus moves to the ✕ button; Escape closes.
  • Dialog: the <dialog> opened with showModal() has the dialog role and modal behaviour, and carries a name (aria-label="Image viewer"). The rest of the page becomes inert: Tab moves between the viewer's buttons and the browser bar, never to the page hidden behind.
  • Focus return: on closing, focus goes back to the thumbnail that opened the image (measured in Chromium, Firefox and WebKit).
  • Alt text: your thumbnail's alt is copied onto the large image. Write it to describe the photo, not “image 1”.
  • Reduced motion: under prefers-reduced-motion: reduce there is no fade and no growth: the image appears and closes at once.

Browser compatibility

Relies on <dialog> and showModal() (Chrome 37, Firefox 98, Safari 15.4) and on aspect-ratio for the thumbnails (Chrome 88, Firefox 89, Safari 15), versions read from @mdn/browser-compat-data. Tested in Chromium 151, Firefox 153 and WebKit 26.5: two galleries on one page, keyboard, touch at 390 px, reduced motion.

Chrome 88+✓ Full (measured)
Firefox 98+✓ Full (measured)
Safari 15.4+✓ Full (measured on WebKit)
Edge 88+✓ Full (Chromium engine)
Mobile iOS 15.4+✓ Full (not measured on a device)
Android Chrome 88+✓ Full (touch measured)

Without showModal() (a browser older than these versions) the script does nothing and the thumbnails stay plain images. Without backdrop-filter (Firefox before 103) the background is 90 % black with no blur.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="fx-0171-root">
  <div class="blb-grid">
    <button type="button" class="blb-thumb" aria-haspopup="dialog"><img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='400' viewBox='0 0 600 400'%3E%3ClinearGradient id='g' x2='1' y2='1'%3E%3Cstop stop-color='%236366f1'/%3E%3Cstop offset='1' stop-color='%238b5cf6'/%3E%3C/linearGradient%3E%3Crect width='600' height='400' fill='url(%23g)'/%3E%3C/svg%3E" alt="Indigo to violet gradient" width="600" height="400"></button>
    <button type="button" class="blb-thumb" aria-haspopup="dialog"><img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='400' viewBox='0 0 600 400'%3E%3ClinearGradient id='g' x2='1' y2='1'%3E%3Cstop stop-color='%23d946ef'/%3E%3Cstop offset='1' stop-color='%23f43f5e'/%3E%3C/linearGradient%3E%3Crect width='600' height='400' fill='url(%23g)'/%3E%3C/svg%3E" alt="Fuchsia to rose gradient" width="600" height="400"></button>
    <button type="button" class="blb-thumb" aria-haspopup="dialog"><img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='400' viewBox='0 0 600 400'%3E%3ClinearGradient id='g' x2='1' y2='1'%3E%3Cstop stop-color='%2306b6d4'/%3E%3Cstop offset='1' stop-color='%2310b981'/%3E%3C/linearGradient%3E%3Crect width='600' height='400' fill='url(%23g)'/%3E%3C/svg%3E" alt="Cyan to emerald gradient" width="600" height="400"></button>
    <button type="button" class="blb-thumb" aria-haspopup="dialog"><img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='400' viewBox='0 0 600 400'%3E%3ClinearGradient id='g' x2='1' y2='1'%3E%3Cstop stop-color='%23f59e0b'/%3E%3Cstop offset='1' stop-color='%23ef4444'/%3E%3C/linearGradient%3E%3Crect width='600' height='400' fill='url(%23g)'/%3E%3C/svg%3E" alt="Amber to red gradient" width="600" height="400"></button>
  </div>
  <dialog class="blb-viewer" aria-label="Image viewer">
    <div class="blb-content">
      <button type="button" class="blb-close" aria-label="Close"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M18 6L6 18M6 6l12 12"/></svg></button>
      <img class="blb-image" alt="">
    </div>
  </dialog>
</div>
CSS
/* fx-0171 — scoped to .fx-0171-root and the blb- prefix: two copies on one page stay independent */
.fx-0171-root,
.fx-0171-root *,
.fx-0171-root *::before,
.fx-0171-root *::after {
  box-sizing: border-box;
}

.fx-0171-root {
  --blb-thumb: 120px;                 /* thumbnail width */
  --blb-gap: 10px;                    /* space between thumbnails */
  --blb-radius: 0.5rem;               /* thumbnail corners */
  --blb-radius-lg: 1rem;              /* large image corners */
  --blb-backdrop: rgba(0, 0, 0, 0.9); /* viewer background */
  --blb-accent: rgba(99, 102, 241, 0.3);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 300px;
  padding: 16px;
  position: relative;
  background: #0a0a0f;
  color: #fff;
  font-family: sans-serif;
}

/* Thumbnails: real buttons wrapping real images */
.blb-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--blb-gap);
  width: 100%;
}
.blb-thumb {
  display: block;
  width: var(--blb-thumb);
  max-width: 100%;
  aspect-ratio: 3 / 2;
  padding: 0;
  border: 0;
  border-radius: var(--blb-radius);
  overflow: hidden;
  background: #1a1a24;
  cursor: pointer;
  transition: transform 0.3s, box-shadow 0.3s;
}
.blb-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.blb-thumb:hover,
.blb-thumb:focus-visible {
  transform: scale(1.05);
  box-shadow: var(--blb-accent) 0 10px 30px;
}
.blb-thumb:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
}

/* Viewer: native modal <dialog> (top layer, focus kept inside, Escape) */
.blb-viewer {
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: hidden;
  background: var(--blb-backdrop);
  color: #fff;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  opacity: 0;
  transition: opacity 0.4s ease;
}
.blb-viewer[open] {
  display: flex;
  align-items: center;
  justify-content: center;
}
.blb-viewer.is-open {
  opacity: 1;
}
.blb-viewer::backdrop {
  background: transparent;
}
.blb-viewer button:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
}

.blb-content {
  position: relative;
  max-width: 80vw;
  transform: scale(0.8);
  transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.blb-viewer.is-open .blb-content {
  transform: scale(1);
}

.blb-image {
  display: block;
  width: auto;
  height: auto;
  max-width: 80vw;
  max-height: 70vh;
  border-radius: var(--blb-radius-lg);
  touch-action: pan-y pinch-zoom;
  -webkit-user-select: none;
  user-select: none;
}

/* Close button */
.blb-close {
  position: absolute;
  top: -44px;
  right: 0;
  width: 36px;
  height: 36px;
  background: rgba(255, 255, 255, 0.1);
  border: none;
  border-radius: 50%;
  color: white;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.3s ease, transform 0.3s ease;
}
.blb-close:hover {
  background: rgba(255, 255, 255, 0.2);
  transform: rotate(90deg);
}

/* Reduced motion: the viewer opens and closes at once, nothing scales */
@media (prefers-reduced-motion: reduce) {
  .blb-thumb,
  .blb-viewer,
  .blb-content,
  .blb-close,
  .blb-nav {
    transition: none;
  }
  .blb-content,
  .blb-thumb:hover,
  .blb-thumb:focus-visible {
    transform: none;
  }
}
JavaScript (fx-0171)
(function () {
  'use strict';
  // Basic Lightbox — one independent instance per .fx-0171-root (pasting the script twice is harmless)
  var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
  var CLOSE_MS = 400; // matches the CSS closing transition

  function setup(root) {
    if (root.hasAttribute('data-blb-ready')) return;
    var thumbs = Array.prototype.slice.call(root.querySelectorAll('.blb-thumb'));
    var viewer = root.querySelector('.blb-viewer');
    if (!thumbs.length || !viewer || typeof viewer.showModal !== 'function') return;
    root.setAttribute('data-blb-ready', '');
    var image = viewer.querySelector('.blb-image');
    var index = 0, opener = null, closeTimer = 0, downOnBackdrop = false;
    image.draggable = false;

    // Large version: data-full on the button, otherwise the thumbnail image itself
    function sourceOf(thumb) {
      var small = thumb.querySelector('img');
      return {
        src: thumb.getAttribute('data-full') || (small ? small.currentSrc || small.src : ''),
        alt: small ? small.alt : ''
      };
    }

    function show(i) {
      index = i;
      var s = sourceOf(thumbs[i]);
      image.src = s.src;
      image.alt = s.alt;
    }

    function open(i) {
      clearTimeout(closeTimer);
      opener = thumbs[i];
      if (!viewer.open) viewer.showModal();
      show(i);
      void viewer.offsetWidth; // start the opening transition from the closed state
      viewer.classList.add('is-open');
    }
    function close() {
      if (!viewer.open) return;
      viewer.classList.remove('is-open');
      clearTimeout(closeTimer);
      if (reduceMotion.matches) viewer.close();
      else closeTimer = setTimeout(function () { viewer.close(); }, CLOSE_MS);
    }

    thumbs.forEach(function (thumb, i) {
      thumb.addEventListener('click', function () { open(i); });
    });
    viewer.querySelector('.blb-close').addEventListener('click', close);

    // Click on the dark background (not on the image or a button) closes
    viewer.addEventListener('pointerdown', function (e) { downOnBackdrop = e.target === viewer; });
    viewer.addEventListener('click', function (e) {
      if (e.target === viewer && downOnBackdrop) close();
    });
    viewer.addEventListener('cancel', function (e) { e.preventDefault(); close(); });

    // Keyboard: Escape closes
    viewer.addEventListener('keydown', function (e) {
      if (e.key === 'Escape') { e.preventDefault(); close(); }
    });

    // Closed: focus goes back to the thumbnail that opened the viewer
    viewer.addEventListener('close', function () {
      clearTimeout(closeTimer);
      viewer.classList.remove('is-open');
      if (opener) opener.focus();
    });
  }

  function start() {
    document.querySelectorAll('.fx-0171-root').forEach(setup);
  }
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
  else start();
})();

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Thumbnail size (CSS — --blb-thumb) 120px Width of each thumbnail (3:2 format). The row wraps onto several lines by itself.
Gap between thumbnails (CSS — --blb-gap) 10px Horizontal and vertical spacing of the grid.
Viewer background (CSS — --blb-backdrop) rgba(0, 0, 0, 0.9) Colour and opacity of the overlay. The ✕ button is white: keep a dark background.
Corners (CSS — --blb-radius / --blb-radius-lg) 0.5rem / 1rem Corners of the thumbnails, then of the large image.
Hover shadow (CSS — --blb-accent) rgba(99, 102, 241, 0.3) Shadow colour when a thumbnail is hovered or has keyboard focus.
Large version (HTML — data-full on the button) absent (the thumbnail image is reused) URL of a bigger image, loaded only when the viewer opens. Handy when the thumbnail is a light preview.
Viewer name (HTML — aria-label of the <dialog>) “Image viewer” What screen readers announce on opening; adapt it to the content (“Access map”).

FAQ

How do I use my own photos?

Replace the src and alt of the <img> inside the .blb-thumb buttons, add or remove buttons: the script finds them all. For a light preview plus a separate large image, put the large one's URL in data-full.

The viewer shows up behind my sticky header, what now?

That should not happen: a <dialog> opened with showModal() goes into the browser's top layer, above every z-index. If you see it, check that the script actually ran (without it nothing opens).

Can it go from one image to the next?

Not this version: it shows one image at a time and closes. For arrows, a counter and finger swipes, use the lightbox with navigation from the same category.