← back to Dw Theme Compact Toolbar
snippets/overlay.lightbox.liquid
473 lines
{% doc %}
Copyright © 2025 Archetype Themes LP. All rights reserved.
A reusable lightbox overlay component that displays content in a full-screen overlay.
Perfect for images, videos, or any content that needs to be viewed in detail.
Overlay
@param {string} [slot] - Content to display in the lightbox
@param {string} [group] - Group identifier for navigation between multiple lightboxes
@example
{% for image in product.images %}
{% capture image_content %}
{{ image | image_url: width: 2400 | image_tag }}
{% endcapture %}
{% render 'overlay.lightbox', slot: image_content, group: 'product-gallery' %}
{% endfor %}
{% enddoc %}
{% liquid
assign group = group | default: ''
%}
<overlay-lightbox data-group="{{ group }}">
<div class="overlay-lightbox__trigger">
{{ slot }}
</div>
<template class="overlay-lightbox__template">
<div class="overlay-lightbox__overlay">
<div class="overlay-lightbox__content">
{{ slot }}
</div>
{% if group != blank %}
{% render 'element.button',
type: 'button',
icon: 'chevron-left',
classlist: 'overlay-lightbox__nav overlay-lightbox__nav--prev',
attributes: 'aria-label="Previous item"',
transparent: true,
style: '--element-button-color-primary: black; --element-button-color-secondary: white'
%}
{% render 'element.button',
type: 'button',
icon: 'chevron-right',
classlist: 'overlay-lightbox__nav overlay-lightbox__nav--next',
attributes: 'aria-label="Next item"',
transparent: true,
style: '--element-button-color-primary: black; --element-button-color-secondary: white'
%}
{% endif %}
{% render 'element.button',
type: 'button',
icon: 'close',
classlist: 'lightbox-close',
attributes: 'aria-label="Close lightbox"',
transparent: true,
style: '--element-button-color-primary: black; --element-button-color-secondary: white'
%}
</div>
</template>
</overlay-lightbox>
{% stylesheet %}
overlay-lightbox {
display: contents;
}
.overlay-lightbox__trigger {
display: contents;
cursor: pointer;
}
.overlay-lightbox__overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.95);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
box-sizing: border-box;
opacity: 0;
visibility: hidden;
}
.overlay-lightbox__overlay--open {
opacity: 1;
visibility: visible;
}
.overlay-lightbox__content {
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.overlay-lightbox__content img {
max-width: 100%;
max-height: 100%;
width: 100%;
height: auto;
object-fit: contain;
}
.overlay-lightbox__nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.overlay-lightbox__nav--prev {
left: 1rem;
}
.overlay-lightbox__nav--next {
right: 1rem;
}
/* Hide navigation buttons when there's only one item in the group */
.overlay-lightbox__overlay--single-item .overlay-lightbox__nav {
display: none;
}
.lightbox-close {
position: absolute;
top: 1rem;
right: 1rem;
}
{% endstylesheet %}
{% javascript %}
class OverlayLightbox extends HTMLElement {
// Static registry to track all lightbox instances
static registry = new Map();
// Track which groups have been prefetched
static prefetchedGroups = new Set();
constructor() {
super();
this.trigger = null;
this.template = null;
this.overlay = null;
this.closeBtn = null;
this.prevBtn = null;
this.nextBtn = null;
this.group = this.dataset.group;
this.groupItems = [];
this.currentIndex = 0;
// Register this instance
if (this.group) {
if (!OverlayLightbox.registry.has(this.group)) {
OverlayLightbox.registry.set(this.group, []);
}
OverlayLightbox.registry.get(this.group).push(this);
}
}
connectedCallback() {
// Initialize elements when connected to DOM
this.trigger = this.querySelector('.overlay-lightbox__trigger');
this.template = this.querySelector('.overlay-lightbox__template');
this.init();
}
init() {
// Open lightbox on trigger click
if (this.trigger) {
this.trigger.addEventListener('click', (e) => {
e.preventDefault();
this.openLightbox();
});
}
// Handle ESC key globally
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isOpen()) {
this.closeLightbox();
} else if (this.isOpen() && this.group) {
if (e.key === 'ArrowLeft') {
e.preventDefault();
this.navigateToPrevious();
} else if (e.key === 'ArrowRight') {
e.preventDefault();
this.navigateToNext();
}
}
});
}
openLightbox() {
// Create overlay if it doesn't exist
if (!this.overlay) {
this.createOverlay();
}
this.overlay.classList.add('overlay-lightbox__overlay--open');
document.body.style.overflow = 'hidden';
// If this is part of a group, find all group items
if (this.group) {
this.findGroupItems();
// Prefetch images for the group
this.prefetchGroupImages();
// Add single-item class if there's only one item in the group
if (this.groupItems.length <= 1) {
this.overlay.classList.add('overlay-lightbox__overlay--single-item');
} else {
this.overlay.classList.remove('overlay-lightbox__overlay--single-item');
}
} else {
// No group, so hide navigation buttons
this.overlay.classList.add('overlay-lightbox__overlay--single-item');
}
}
createOverlay() {
// Check if template exists
if (!this.template) {
console.error('OverlayLightbox: Template not found');
return;
}
// Clone the template content
this.overlay = this.template.content.cloneNode(true).querySelector('.overlay-lightbox__overlay');
// Check if overlay was created successfully
if (!this.overlay) {
console.error('OverlayLightbox: Overlay element not found in template');
return;
}
// Get references to buttons
this.closeBtn = this.overlay.querySelector('.lightbox-close');
this.prevBtn = this.overlay.querySelector('.overlay-lightbox__nav--prev');
this.nextBtn = this.overlay.querySelector('.overlay-lightbox__nav--next');
// Add event listeners
if (this.closeBtn) {
this.closeBtn.addEventListener('click', (e) => {
e.preventDefault();
this.closeLightbox();
});
}
if (this.prevBtn) {
this.prevBtn.addEventListener('click', (e) => {
e.preventDefault();
this.navigateToPrevious();
});
}
if (this.nextBtn) {
this.nextBtn.addEventListener('click', (e) => {
e.preventDefault();
this.navigateToNext();
});
}
// Close lightbox when clicking outside content
if (this.overlay) {
this.overlay.addEventListener('click', (e) => {
if (e.target === this.overlay) {
this.closeLightbox();
}
});
}
// Append to body
if (this.overlay) {
document.body.appendChild(this.overlay);
}
}
closeLightbox() {
if (this.overlay) {
this.overlay.classList.remove('overlay-lightbox__overlay--open');
this.overlay.classList.remove('overlay-lightbox__overlay--single-item');
// Remove overlay immediately
if (this.overlay.parentNode) {
this.overlay.parentNode.removeChild(this.overlay);
this.overlay = null;
this.closeBtn = null;
this.prevBtn = null;
this.nextBtn = null;
}
}
document.body.style.overflow = '';
}
findGroupItems() {
// Use the static registry to find group items
this.groupItems = OverlayLightbox.registry.get(this.group) || [];
this.currentIndex = this.groupItems.indexOf(this);
}
navigateToPrevious() {
if (!this.group || this.groupItems.length <= 1) return;
let targetIndex = this.currentIndex - 1;
if (targetIndex < 0) {
// Loop to last item
targetIndex = this.groupItems.length - 1;
}
const targetLightbox = this.groupItems[targetIndex];
if (targetLightbox) {
this.switchToLightbox(targetLightbox);
}
}
navigateToNext() {
if (!this.group || this.groupItems.length <= 1) return;
let targetIndex = this.currentIndex + 1;
if (targetIndex >= this.groupItems.length) {
// Loop to first item
targetIndex = 0;
}
const targetLightbox = this.groupItems[targetIndex];
if (targetLightbox) {
this.switchToLightbox(targetLightbox);
}
}
switchToLightbox(targetLightbox) {
// Close current lightbox
this.closeLightbox();
// Open target lightbox immediately
targetLightbox.openLightbox();
}
prefetchGroupImages() {
// Only prefetch once per group
if (this.group && !OverlayLightbox.prefetchedGroups.has(this.group)) {
OverlayLightbox.prefetchedGroups.add(this.group);
this.groupItems.forEach(lightbox => {
// Find all images in this lightbox's content
const images = lightbox.querySelectorAll('img');
images.forEach(img => {
// Prefetch the largest available image
this.prefetchLargestImage(img);
});
});
}
}
prefetchLargestImage(img) {
// Try to get the largest image from srcset first
if (img.srcset) {
const srcsetUrls = img.srcset.split(',').map(src => src.trim().split(' ')[0]);
// Find the largest size (highest number in the URL)
let largestUrl = img.src;
let largestSize = 0;
srcsetUrls.forEach(url => {
const size = this.extractImageSize(url);
if (size > largestSize) {
largestSize = size;
largestUrl = url;
}
});
this.prefetchImage(largestUrl);
} else {
// No srcset, try to get largest size from original URL
const largestUrl = this.getLargestImageUrl(img.src);
this.prefetchImage(largestUrl);
}
}
extractImageSize(url) {
try {
const urlObj = new URL(url);
// Check for width parameter
if (urlObj.searchParams.has('width')) {
return parseInt(urlObj.searchParams.get('width')) || 0;
}
// Check for underscore format (image_2400x.jpg)
const pathParts = urlObj.pathname.split('_');
if (pathParts.length > 1) {
const sizeMatch = pathParts[pathParts.length - 1].match(/(\d+)x/);
if (sizeMatch) {
return parseInt(sizeMatch[1]) || 0;
}
}
} catch (e) {
// URL parsing failed
}
return 0;
}
getLargestImageUrl(originalUrl) {
try {
const url = new URL(originalUrl);
// Handle Shopify image URLs - try to get 2400px version
if (url.searchParams.has('width')) {
url.searchParams.set('width', '2400');
return url.toString();
} else if (url.pathname.includes('_')) {
// Handle Shopify's underscore format
const pathParts = url.pathname.split('_');
if (pathParts.length > 1) {
const extension = pathParts[pathParts.length - 1].split('.')[1] || 'jpg';
const newPath = pathParts.slice(0, -1).join('_') + '_2400x.' + extension;
url.pathname = newPath;
return url.toString();
}
}
} catch (e) {
// If URL parsing fails, return original
}
return originalUrl;
}
prefetchImage(url) {
const prefetchImg = new Image();
prefetchImg.src = url;
}
isOpen() {
return this.overlay && this.overlay.classList.contains('overlay-lightbox__overlay--open');
}
// Clean up when element is removed
disconnectedCallback() {
if (this.group) {
const groupItems = OverlayLightbox.registry.get(this.group);
if (groupItems) {
const index = groupItems.indexOf(this);
if (index > -1) {
groupItems.splice(index, 1);
}
// Remove empty groups
if (groupItems.length === 0) {
OverlayLightbox.registry.delete(this.group);
}
}
}
}
}
// Define the custom element
customElements.define('overlay-lightbox', OverlayLightbox);
{% endjavascript %}