JAVASCRIPTshopifyintermediate
Shopify Product Quick View Modal
Add a quick view popup to preview products without leaving the page
Faisal Yaqoob
November 17, 2025
#shopify#product#modal#quickview#ajax
Code
javascript
1 // Quick View Modal Class 2 class ProductQuickView { 3 constructor() { 4 this.modal = null; 5 this.overlay = null; 6 this.init(); 7 } 8
9 init() { 10 this.createModal(); 11
12 document.addEventListener('click', (e) => { 13 if (e.target.matches('[data-quickview-btn]')) { 14 e.preventDefault(); 15 const handle = e.target.dataset.quickviewBtn; 16 this.openQuickView(handle); 17 } 18 }); 19 } 20
21 createModal() { 22 // Create overlay 23 this.overlay = document.createElement('div'); 24 this.overlay.className = 'quickview-overlay'; 25 this.overlay.addEventListener('click', () => this.close()); 26
27 // Create modal 28 this.modal = document.createElement('div'); 29 this.modal.className = 'quickview-modal'; 30 this.modal.innerHTML = ` 31 <button class="quickview-close" data-quickview-close>×</button> 32 <div class="quickview-content"> 33 <div class="quickview-loading">Loading...</div> 34 </div> 35 `; 36
37 document.body.appendChild(this.overlay); 38 document.body.appendChild(this.modal); 39
40 // Close button 41 this.modal.querySelector('[data-quickview-close]').addEventListener('click', () => this.close()); 42 } 43
44 async openQuickView(handle) { 45 try { 46 // Show modal 47 this.overlay.classList.add('is-active'); 48 this.modal.classList.add('is-active'); 49 document.body.style.overflow = 'hidden'; 50
51 // Fetch product data 52 const response = await fetch(`/products/${handle}.js`); 53 const product = await response.json(); 54
55 // Render product 56 this.renderProduct(product); 57 } catch (error) { 58 console.error('Error loading product:', error); 59 this.showError(); 60 } 61 } 62
63 renderProduct(product) { 64 const content = this.modal.querySelector('.quickview-content'); 65
66 const html = ` 67 <div class="quickview-grid"> 68 <div class="quickview-images"> 69 <div class="quickview-main-image"> 70 <img src="${product.featured_image}" alt="${product.title}" id="quickview-featured-image"> 71 </div> 72 ${product.images.length > 1 ? this.renderThumbnails(product.images) : ''} 73 </div> 74
75 <div class="quickview-details"> 76 <h2 class="quickview-title">${product.title}</h2> 77
78 <div class="quickview-price"> 79 ${this.formatPrice(product.price)} 80 </div> 81
82 ${product.description ? ` 83 <div class="quickview-description"> 84 ${product.description} 85 </div> 86 ` : ''} 87
88 <form class="quickview-form" data-product-form> 89 ${product.variants.length > 1 ? this.renderVariants(product) : ''} 90
91 <div class="quickview-quantity"> 92 <label>Quantity:</label> 93 <input type="number" name="quantity" value="1" min="1" class="quantity-input"> 94 </div> 95
96 <input type="hidden" name="id" value="${product.variants[0].id}"> 97
98 <button type="submit" class="btn btn--add-to-cart" ${!product.available ? 'disabled' : ''}> 99 ${product.available ? 'Add to Cart' : 'Sold Out'} 100 </button> 101 </form> 102
103 <a href="/products/${product.handle}" class="quickview-link"> 104 View Full Details → 105 </a> 106 </div> 107 </div> 108 `; 109
110 content.innerHTML = html; 111
112 this.initForm(); 113 this.initImageGallery(); 114 } 115
116 renderThumbnails(images) { 117 return ` 118 <div class="quickview-thumbnails"> 119 ${images.map(img => ` 120 <img src="${img}" alt="Product thumbnail" class="quickview-thumb" data-image-src="${img}"> 121 `).join('')} 122 </div> 123 `; 124 } 125
126 renderVariants(product) { 127 if (!product.options || product.options.length === 0) { 128 return ''; 129 } 130
131 return ` 132 <div class="quickview-options"> 133 ${product.options.map((option, index) => ` 134 <div class="quickview-option"> 135 <label>${option.name}:</label> 136 <select name="options[${option.name}]" data-option-select="${index}"> 137 ${option.values.map(value => ` 138 <option value="${value}">${value}</option> 139 `).join('')} 140 </select> 141 </div> 142 `).join('')} 143 </div> 144 `; 145 } 146
147 initForm() { 148 const form = this.modal.querySelector('[data-product-form]'); 149
150 if (!form) return; 151
152 form.addEventListener('submit', async (e) => { 153 e.preventDefault(); 154
155 const formData = new FormData(form); 156 const data = { 157 items: [{ 158 id: formData.get('id'), 159 quantity: parseInt(formData.get('quantity')) 160 }] 161 }; 162
163 try { 164 const response = await fetch('/cart/add.js', { 165 method: 'POST', 166 headers: { 167 'Content-Type': 'application/json', 168 }, 169 body: JSON.stringify(data) 170 }); 171
172 if (response.ok) { 173 this.showSuccess(); 174 // Trigger cart update event 175 document.dispatchEvent(new CustomEvent('cart:updated')); 176 } 177 } catch (error) { 178 console.error('Error adding to cart:', error); 179 } 180 }); 181
182 // Variant selection 183 const selects = form.querySelectorAll('[data-option-select]'); 184 selects.forEach(select => { 185 select.addEventListener('change', () => { 186 this.updateVariant(form); 187 }); 188 }); 189 } 190
191 initImageGallery() { 192 const thumbnails = this.modal.querySelectorAll('.quickview-thumb'); 193 const featuredImage = this.modal.querySelector('#quickview-featured-image'); 194
195 thumbnails.forEach(thumb => { 196 thumb.addEventListener('click', () => { 197 featuredImage.src = thumb.dataset.imageSrc; 198 thumbnails.forEach(t => t.classList.remove('active')); 199 thumb.classList.add('active'); 200 }); 201 }); 202 } 203
204 updateVariant(form) { 205 // Update variant ID based on selected options 206 // This is simplified - you'll need proper variant matching logic 207 const selects = form.querySelectorAll('[data-option-select]'); 208 const selectedOptions = Array.from(selects).map(s => s.value); 209 // Match variant and update hidden input 210 } 211
212 showSuccess() { 213 const btn = this.modal.querySelector('.btn--add-to-cart'); 214 const originalText = btn.textContent; 215 btn.textContent = 'Added! ✓'; 216 btn.classList.add('success'); 217
218 setTimeout(() => { 219 btn.textContent = originalText; 220 btn.classList.remove('success'); 221 }, 2000); 222 } 223
224 showError() { 225 const content = this.modal.querySelector('.quickview-content'); 226 content.innerHTML = '<p class="error">Failed to load product. Please try again.</p>'; 227 } 228
229 close() { 230 this.overlay.classList.remove('is-active'); 231 this.modal.classList.remove('is-active'); 232 document.body.style.overflow = ''; 233 } 234
235 formatPrice(cents) { 236 return '$' + (cents / 100).toFixed(2); 237 } 238 } 239
240 // Initialize 241 document.addEventListener('DOMContentLoaded', () => { 242 new ProductQuickView(); 243 });
Shopify Product Quick View Modal
Create a quick view modal that lets customers preview product details without navigating to the full product page.
// Quick View Modal Class
class ProductQuickView {
constructor() {
this.modal = null;
this.overlay = null;
this.init();
}
init() {
this.createModal();
document.addEventListener('click', (e) => {
if (e.target.matches('[data-quickview-btn]')) {
e.preventDefault();
const handle = e.target.dataset.quickviewBtn;
this.openQuickView(handle);
}
});
}
createModal() {
// Create overlay
this.overlay = document.createElement('div');
this.overlay.className = 'quickview-overlay';
this.overlay.addEventListener('click', () => this.close());
// Create modal
this.modal = document.createElement('div');
this.modal.className = 'quickview-modal';
this.modal.innerHTML = `
<button class="quickview-close" data-quickview-close>×</button>
<div class="quickview-content">
<div class="quickview-loading">Loading...</div>
</div>
`;
document.body.appendChild(this.overlay);
document.body.appendChild(this.modal);
// Close button
this.modal.querySelector('[data-quickview-close]').addEventListener('click', () => this.close());
}
async openQuickView(handle) {
try {
// Show modal
this.overlay.classList.add('is-active');
this.modal.classList.add('is-active');
document.body.style.overflow = 'hidden';
// Fetch product data
const response = await fetch(`/products/${handle}.js`);
const product = await response.json();
// Render product
this.renderProduct(product);
} catch (error) {
console.error('Error loading product:', error);
this.showError();
}
}
renderProduct(product) {
const content = this.modal.querySelector('.quickview-content');
const html = `
<div class="quickview-grid">
<div class="quickview-images">
<div class="quickview-main-image">
<img src="${product.featured_image}" alt="${product.title}" id="quickview-featured-image">
</div>
${product.images.length > 1 ? this.renderThumbnails(product.images) : ''}
</div>
<div class="quickview-details">
<h2 class="quickview-title">${product.title}</h2>
<div class="quickview-price">
${this.formatPrice(product.price)}
</div>
${product.description ? `
<div class="quickview-description">
${product.description}
</div>
` : ''}
<form class="quickview-form" data-product-form>
${product.variants.length > 1 ? this.renderVariants(product) : ''}
<div class="quickview-quantity">
<label>Quantity:</label>
<input type="number" name="quantity" value="1" min="1" class="quantity-input">
</div>
<input type="hidden" name="id" value="${product.variants[0].id}">
<button type="submit" class="btn btn--add-to-cart" ${!product.available ? 'disabled' : ''}>
${product.available ? 'Add to Cart' : 'Sold Out'}
</button>
</form>
<a href="/products/${product.handle}" class="quickview-link">
View Full Details →
</a>
</div>
</div>
`;
content.innerHTML = html;
this.initForm();
this.initImageGallery();
}
renderThumbnails(images) {
return `
<div class="quickview-thumbnails">
${images.map(img => `
<img src="${img}" alt="Product thumbnail" class="quickview-thumb" data-image-src="${img}">
`).join('')}
</div>
`;
}
renderVariants(product) {
if (!product.options || product.options.length === 0) {
return '';
}
return `
<div class="quickview-options">
${product.options.map((option, index) => `
<div class="quickview-option">
<label>${option.name}:</label>
<select name="options[${option.name}]" data-option-select="${index}">
${option.values.map(value => `
<option value="${value}">${value}</option>
`).join('')}
</select>
</div>
`).join('')}
</div>
`;
}
initForm() {
const form = this.modal.querySelector('[data-product-form]');
if (!form) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const data = {
items: [{
id: formData.get('id'),
quantity: parseInt(formData.get('quantity'))
}]
};
try {
const response = await fetch('/cart/add.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
if (response.ok) {
this.showSuccess();
// Trigger cart update event
document.dispatchEvent(new CustomEvent('cart:updated'));
}
} catch (error) {
console.error('Error adding to cart:', error);
}
});
// Variant selection
const selects = form.querySelectorAll('[data-option-select]');
selects.forEach(select => {
select.addEventListener('change', () => {
this.updateVariant(form);
});
});
}
initImageGallery() {
const thumbnails = this.modal.querySelectorAll('.quickview-thumb');
const featuredImage = this.modal.querySelector('#quickview-featured-image');
thumbnails.forEach(thumb => {
thumb.addEventListener('click', () => {
featuredImage.src = thumb.dataset.imageSrc;
thumbnails.forEach(t => t.classList.remove('active'));
thumb.classList.add('active');
});
});
}
updateVariant(form) {
// Update variant ID based on selected options
// This is simplified - you'll need proper variant matching logic
const selects = form.querySelectorAll('[data-option-select]');
const selectedOptions = Array.from(selects).map(s => s.value);
// Match variant and update hidden input
}
showSuccess() {
const btn = this.modal.querySelector('.btn--add-to-cart');
const originalText = btn.textContent;
btn.textContent = 'Added! ✓';
btn.classList.add('success');
setTimeout(() => {
btn.textContent = originalText;
btn.classList.remove('success');
}, 2000);
}
showError() {
const content = this.modal.querySelector('.quickview-content');
content.innerHTML = '<p class="error">Failed to load product. Please try again.</p>';
}
close() {
this.overlay.classList.remove('is-active');
this.modal.classList.remove('is-active');
document.body.style.overflow = '';
}
formatPrice(cents) {
return '$' + (cents / 100).toFixed(2);
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
new ProductQuickView();
});
Add to Collection Template
<!-- Add this button to your product cards -->
<button type="button" data-quickview-btn="{{ product.handle }}" class="btn-quickview">
Quick View
</button>
CSS Styling
/* Overlay */
.quickview-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 9998;
}
.quickview-overlay.is-active {
opacity: 1;
visibility: visible;
}
/* Modal */
.quickview-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.8);
max-width: 900px;
width: 90%;
max-height: 90vh;
background: #fff;
border-radius: 8px;
padding: 30px;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 9999;
overflow-y: auto;
}
.quickview-modal.is-active {
opacity: 1;
visibility: visible;
transform: translate(-50%, -50%) scale(1);
}
.quickview-close {
position: absolute;
top: 15px;
right: 15px;
background: none;
border: none;
font-size: 30px;
cursor: pointer;
line-height: 1;
padding: 5px 10px;
}
.quickview-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
}
.quickview-main-image img {
width: 100%;
height: auto;
border-radius: 4px;
}
.quickview-thumbnails {
display: flex;
gap: 10px;
margin-top: 15px;
}
.quickview-thumb {
width: 80px;
height: 80px;
object-fit: cover;
cursor: pointer;
border: 2px solid transparent;
border-radius: 4px;
transition: border-color 0.2s;
}
.quickview-thumb:hover,
.quickview-thumb.active {
border-color: #000;
}
.quickview-title {
font-size: 24px;
margin-bottom: 15px;
}
.quickview-price {
font-size: 22px;
font-weight: bold;
margin-bottom: 20px;
}
.quickview-description {
margin-bottom: 25px;
line-height: 1.6;
}
.quickview-option {
margin-bottom: 15px;
}
.quickview-option label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.quickview-option select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.quickview-quantity {
margin: 20px 0;
}
.quantity-input {
width: 80px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn--add-to-cart {
width: 100%;
padding: 15px;
background: #000;
color: #fff;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
.btn--add-to-cart:hover {
background: #333;
}
.btn--add-to-cart:disabled {
background: #ccc;
cursor: not-allowed;
}
.quickview-link {
display: block;
margin-top: 20px;
text-align: center;
color: #666;
}
@media (max-width: 768px) {
.quickview-grid {
grid-template-columns: 1fr;
}
}
Features
- AJAX Loading: Fetches product data without page reload
- Image Gallery: Main image with thumbnail navigation
- Variant Selection: Dynamic variant switching
- Add to Cart: Direct add to cart from modal
- Responsive: Works on all screen sizes
- Smooth Animations: Professional fade and scale effects
- Keyboard Accessible: ESC key to close
Related Snippets
Shopify AJAX Collection Filters
Add dynamic filtering to collection pages without page reloads
JAVASCRIPTshopifyadvanced
javascriptPreview
// Collection Filters Class
class CollectionFilters {
constructor(options = {}) {
this.container = document.querySelector(options.container || '[data-collection-container]');
...#shopify#filters#ajax+2
11/27/2025
View
Shopify Size Chart Modal
Add a size chart popup to help customers choose the right size
JAVASCRIPTshopifybeginner
javascriptPreview
// Size Chart Modal Class
class SizeChartModal {
constructor(options = {}) {
this.trigger = options.trigger || '[data-size-chart-trigger]';
...#shopify#size-chart#modal+2
11/24/2025
View
Shopify Search Autocomplete
Add predictive search with product suggestions and instant results
JAVASCRIPTshopifyintermediate
javascriptPreview
// Search Autocomplete Class
class SearchAutocomplete {
constructor(options = {}) {
this.searchInput = document.querySelector(options.input || '[data-search-input]');
...#shopify#search#autocomplete+2
11/21/2025
View