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
Minimal viewer: a clicked thumbnail opens large in a real dialog over a blurred black background; ✕, a click beside it or Escape closes it.
Updated
This effect is free — the Gallery category contains 44 effects total, including 6 free. Effect.Labs has 840 vanilla effects. Explore the category →
12 Tanners' Quay · doors open at 7:30 pm
Access map — tap to enlarge
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.
<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.alt is copied onto the large image. Write it to describe the photo, not “image 1”.prefers-reduced-motion: reduce there is no fade and no growth: the image appears and closes at once.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.
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.
Copy the three blocks into your page. No dependencies.
<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>
/* 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;
}
}
(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();
})();
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
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”). |
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.
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).
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.