The unexpected fix: Solving the tiny image mystery in Lightbox Studio
Every so often, a bug report comes in that’s less of a bug and more of a case of sheer dumb luck. This was one of those.
A new client reached out because after installing Lightbox Studio, their images suddenly looked “too small.” Normally, Lightbox Studio displays images in their real size with zoom, so this was a bit surprising. Turns out, the issue wasn’t the plugin—it was how Squarespace’s stock lightbox handles gallery section images.
The Lucky Break
Squarespace has two types of lightboxes: one for gallery blocks and another for gallery sections. The gallery section lightbox stretches images to fill the viewport, which usually makes sense. But in this case, the user had uploaded tiny 150px-wide images—just solid color squares. With Squarespace’s auto-scaling, they expanded to fullscreen, and since they were plain colors, there was no visible quality loss. A perfect illusion.
Let’s look at another example, where I use a tiny picture to show how the image scaling really works. This time, instead of a plain color, I uploaded a small, detailed image.
Let’s see how the image in the Gallery section lightbox gets scaled—resulting in a blurry display.
Then Lightbox Studio came in and did exactly what it was supposed to do—display images at their actual size. And suddenly, 150px looked like… 150px.
Fixing What Wasn’t Really Broken
Technically, this wasn’t a bug. But I still wanted to see if I could smooth things out.
The simple, straightforward solution would be to re-upload the images at a larger size. But that wasn’t really an option—there were hundreds of plain color images scattered across different galleries on the site, making it impossible to fix manually.
I also tried scaling the images with CSS, but that introduced new problems. It not only affected the images but also scaled the lightbox captions and broke positioning, which wasn’t acceptable.
Instead of forcing unnatural stretching, the solution involved dynamically converting these tiny images to base64 and resizing them on the fly. This way, the images could be handled more gracefully while keeping normal-sized galleries unaffected.
A queue system was implemented to process only relevant images and avoid unnecessary overhead. The end result? A smart fix for an issue that wasn’t really an issue—just an example of how sometimes, luck (and a bit of coding) can make things work in unexpected ways.
The result? No more tiny images, and the zoom feature remains useful.
Let’s take a look at the video below—where I added a red color to processed images for demonstration. Instead of displaying a naturally small image, Lightbox Studio now loads the larger base64 variant for a seamless viewing experience.
For those interested in the code, take a look below. 👇
const validGalleries = [ 'section[data-section-id="679b91707efb8d59caa51da9"]', 'section[data-section-id="679cf47e749b1240e42de9b4"]', ]; function getBase64FromImage(url) { return new Promise((resolve, reject) => { var xhr = new XMLHttpRequest(); xhr.responseType = "arraybuffer"; xhr.open("GET", url); xhr.onload = function () { if (xhr.status !== 200) { return reject(new Error(`Failed to load image: ${xhr.statusText}`)); } var bytes = new Uint8Array(xhr.response); var binary = String.fromCharCode.apply(null, bytes); var mediaType = xhr.getResponseHeader('content-type'); var base64 = `data:${mediaType};base64,${btoa(binary)}`; resolve(base64); }; xhr.onerror = () => reject(new Error("Network error")); xhr.send(); }); } function resizeBase64Image(base64Image) { return new Promise((resolve, reject) => { const maxSizeInMB = 4; const maxSizeInBytes = maxSizeInMB * 1024 * 1024; const img = new Image(); img.src = base64Image; img.onload = function () { const canvas = document.createElement("canvas"); const ctx = canvas.getContext('2d'); const width = img.width; const height = img.height; const aspectRatio = width / height; const newWidth = Math.sqrt(maxSizeInBytes * aspectRatio); const newHeight = Math.sqrt(maxSizeInBytes / aspectRatio); canvas.width = newWidth; canvas.height = newHeight; ctx.drawImage(img, 0, 0, newWidth, newHeight); let quality = 0.8; let dataURL = canvas.toDataURL('image/jpeg', quality); resolve(dataURL); }; }); } async function processGalleryImage(img) { const gallery = img.closest(validGalleries.join(', ')); if (!gallery || !img) return; try { const imageSrc = img.src; const cachedBase64 = localStorage.getItem(imageSrc); if (cachedBase64) { const resizedBase64 = await resizeBase64Image(cachedBase64); attachBase64ToBinder(img, resizedBase64); } else { const base64 = await getBase64FromImage(imageSrc); const resizedBase64 = await resizeBase64Image(base64); localStorage.setItem(imageSrc, base64); attachBase64ToBinder(img, resizedBase64); } } catch (error) { console.error("Error processing image:", error); } } // Queue system for processing images class ImageProcessingQueue { constructor() { this.queue = []; this.isProcessing = false; this.batchSize = 10; } add(images) { this.queue.push(...images); this.process(); } async process() { if (this.isProcessing || this.queue.length === 0) return; this.isProcessing = true; const batch = this.queue.splice(0, this.batchSize); try { await Promise.all(batch.map(img => processGalleryImage(img))); } catch (error) { console.error('Error processing batch:', error); } this.isProcessing = false; // Process next batch if queue is not empty if (this.queue.length > 0) { setTimeout(() => this.process(), 100); } } } const processingQueue = new ImageProcessingQueue(); // Process surrounding images async function processSurroundingImages(currentImg) { const gallery = currentImg.closest(validGalleries.join(', ')); if (!gallery) return; const allImages = Array.from(gallery.querySelectorAll('.gallery-grid-lightbox-link img')); const currentIndex = allImages.indexOf(currentImg); if (currentIndex === -1) return; const startIndex = Math.max(0, currentIndex - 5); const endIndex = Math.min(allImages.length, currentIndex + 6); const surroundingImages = allImages.slice(startIndex, endIndex); processingQueue.add(surroundingImages); } async function attachBase64ToBinder(img, base64) { const binder = img.closest('[data-lightbox-binder-id]'); if (!binder) return; if (!binder.hasAttribute('data-has-events')) { binder.addEventListener('touchstart', () => processSurroundingImages(img), { once: true }); binder.addEventListener('mouseenter', () => processSurroundingImages(img), { once: true }); binder.setAttribute('data-has-events', 'true'); } binder.classList.add('is-base64-binder'); binder.setAttribute('href', base64); } // Intersection Observer to detect visible images const intersectionObserver = new IntersectionObserver((entries) => { const visibleImages = entries .filter(entry => entry.isIntersecting) .map(entry => entry.target); if (visibleImages.length > 0) { processingQueue.add(visibleImages); visibleImages.forEach(img => intersectionObserver.unobserve(img)); } }, { rootMargin: '50px 0px', threshold: 0.1 }); // Function to start observing an image function observeImage(img) { if (img && img.nodeType === 1) { intersectionObserver.observe(img); } } // Mutation Observer to detect new elements const mutationObserver = new MutationObserver(mutations => { mutations.forEach((mutationRecord) => { if (mutationRecord.type === 'childList' && mutationRecord.addedNodes.length > 0) { mutationRecord.addedNodes.forEach(node => { if (node.nodeType === 1 && node.matches('[data-lightbox-binder-id]')) { const img = node.querySelector('img'); if (img) observeImage(img); } }); } else if (mutationRecord.type === 'attributes' && mutationRecord.attributeName === 'data-lightbox-binder-id') { const img = mutationRecord.target.querySelector('img'); if (img) observeImage(img); } }); }); // Observe the document for changes mutationObserver.observe(document.body, { childList: true, subtree: true, attributeFilter: ['data-lightbox-binder-id'] }); // Initial setup for existing images document.querySelectorAll('.gallery-grid-lightbox-link img').forEach(img => { observeImage(img); });
Take away
It’s funny how some "issues" only exist because of lucky accidents. This was one of those cases where a quirk in the stock lightbox made everything seem fine—until a tool that follows actual logic came along. But hey, that’s the kind of thing that keeps development interesting!