JAVASCRIPTshopifyintermediate
Shopify Wishlist with LocalStorage
Add wishlist/favorites functionality without requiring a Shopify app
Faisal Yaqoob
November 28, 2025
#shopify#wishlist#favorites#localstorage
Code
javascript
1 // Wishlist Class 2 class Wishlist { 3 constructor(options = {}) { 4 this.storageKey = options.storageKey || 'shopify_wishlist'; 5 this.wishlistPage = document.querySelector(options.wishlistPage || '[data-wishlist-container]'); 6
7 this.init(); 8 } 9
10 init() { 11 // Bind add to wishlist buttons 12 document.addEventListener('click', (e) => { 13 if (e.target.matches('[data-wishlist-add]') || e.target.closest('[data-wishlist-add]')) { 14 e.preventDefault(); 15 const btn = e.target.closest('[data-wishlist-add]') || e.target; 16 this.toggleWishlist(btn); 17 } 18 }); 19
20 // Update all wishlist buttons 21 this.updateAllButtons(); 22
23 // Update wishlist count 24 this.updateWishlistCount(); 25
26 // Render wishlist page if on wishlist page 27 if (this.wishlistPage) { 28 this.renderWishlistPage(); 29 } 30 } 31
32 toggleWishlist(button) { 33 const productId = button.dataset.wishlistAdd; 34 const productHandle = button.dataset.productHandle; 35 const productTitle = button.dataset.productTitle; 36 const productImage = button.dataset.productImage; 37 const productPrice = button.dataset.productPrice; 38
39 if (!productId) return; 40
41 const wishlist = this.getWishlist(); 42 const index = wishlist.findIndex(item => item.id === productId); 43
44 if (index > -1) { 45 // Remove from wishlist 46 wishlist.splice(index, 1); 47 this.showNotification(`${productTitle} removed from wishlist`); 48 } else { 49 // Add to wishlist 50 wishlist.push({ 51 id: productId, 52 handle: productHandle, 53 title: productTitle, 54 image: productImage, 55 price: productPrice, 56 addedAt: Date.now() 57 }); 58 this.showNotification(`${productTitle} added to wishlist`); 59 } 60
61 this.saveWishlist(wishlist); 62 this.updateAllButtons(); 63 this.updateWishlistCount(); 64
65 // Refresh wishlist page if open 66 if (this.wishlistPage) { 67 this.renderWishlistPage(); 68 } 69 } 70
71 getWishlist() { 72 try { 73 const data = localStorage.getItem(this.storageKey); 74 return data ? JSON.parse(data) : []; 75 } catch (error) { 76 console.error('Error reading wishlist:', error); 77 return []; 78 } 79 } 80
81 saveWishlist(wishlist) { 82 try { 83 localStorage.setItem(this.storageKey, JSON.stringify(wishlist)); 84
85 // Trigger custom event 86 document.dispatchEvent(new CustomEvent('wishlist:updated', { 87 detail: { wishlist } 88 })); 89 } catch (error) { 90 console.error('Error saving wishlist:', error); 91 } 92 } 93
94 isInWishlist(productId) { 95 const wishlist = this.getWishlist(); 96 return wishlist.some(item => item.id === productId); 97 } 98
99 updateAllButtons() { 100 const buttons = document.querySelectorAll('[data-wishlist-add]'); 101
102 buttons.forEach(button => { 103 const productId = button.dataset.wishlistAdd; 104 const isInWishlist = this.isInWishlist(productId); 105
106 if (isInWishlist) { 107 button.classList.add('in-wishlist'); 108 button.setAttribute('aria-label', 'Remove from wishlist'); 109 } else { 110 button.classList.remove('in-wishlist'); 111 button.setAttribute('aria-label', 'Add to wishlist'); 112 } 113 }); 114 } 115
116 updateWishlistCount() { 117 const wishlist = this.getWishlist(); 118 const countElements = document.querySelectorAll('[data-wishlist-count]'); 119
120 countElements.forEach(el => { 121 el.textContent = wishlist.length; 122 el.style.display = wishlist.length > 0 ? 'inline' : 'none'; 123 }); 124 } 125
126 async renderWishlistPage() { 127 const wishlist = this.getWishlist(); 128
129 if (wishlist.length === 0) { 130 this.wishlistPage.innerHTML = this.getEmptyTemplate(); 131 return; 132 } 133
134 // Fetch fresh product data 135 const products = await this.fetchProducts(wishlist.map(item => item.handle)); 136
137 this.wishlistPage.innerHTML = this.getWishlistTemplate(products); 138
139 // Bind remove buttons 140 this.wishlistPage.querySelectorAll('[data-remove-wishlist]').forEach(btn => { 141 btn.addEventListener('click', () => { 142 const productId = btn.dataset.removeWishlist; 143 this.removeFromWishlist(productId); 144 }); 145 }); 146 } 147
148 async fetchProducts(handles) { 149 const products = []; 150
151 for (const handle of handles) { 152 try { 153 const response = await fetch(`/products/${handle}.js`); 154 const product = await response.json(); 155 products.push(product); 156 } catch (error) { 157 console.error(`Error fetching product ${handle}:`, error); 158 } 159 } 160
161 return products; 162 } 163
164 getWishlistTemplate(products) { 165 return ` 166 <div class="wishlist-header"> 167 <h1>My Wishlist</h1> 168 <p>${products.length} item${products.length !== 1 ? 's' : ''}</p> 169 </div> 170
171 <div class="wishlist-grid"> 172 ${products.map(product => ` 173 <div class="wishlist-item"> 174 <button 175 class="wishlist-remove" 176 data-remove-wishlist="${product.id}" 177 aria-label="Remove from wishlist"> 178 × 179 </button> 180
181 <a href="/products/${product.handle}" class="wishlist-item-link"> 182 <div class="wishlist-item-image"> 183 <img src="${product.featured_image}" alt="${product.title}"> 184 </div> 185
186 <div class="wishlist-item-info"> 187 <h3 class="wishlist-item-title">${product.title}</h3> 188
189 <div class="wishlist-item-price"> 190 ${this.formatPrice(product.price)} 191 </div> 192
193 ${product.available ? 194 `<button class="btn btn--add-to-cart" onclick="addToCart(${product.variants[0].id}); return false;"> 195 Add to Cart 196 </button>` : 197 `<p class="sold-out">Sold Out</p>` 198 } 199 </div> 200 </a> 201 </div> 202 `).join('')} 203 </div> 204 `; 205 } 206
207 getEmptyTemplate() { 208 return ` 209 <div class="wishlist-empty"> 210 <svg width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 211 <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path> 212 </svg> 213
214 <h2>Your Wishlist is Empty</h2> 215 <p>Save your favorite products to your wishlist</p> 216
217 <a href="/collections/all" class="btn">Continue Shopping</a> 218 </div> 219 `; 220 } 221
222 removeFromWishlist(productId) { 223 const wishlist = this.getWishlist(); 224 const filtered = wishlist.filter(item => item.id !== productId); 225
226 this.saveWishlist(filtered); 227 this.updateAllButtons(); 228 this.updateWishlistCount(); 229 this.renderWishlistPage(); 230 } 231
232 clearWishlist() { 233 this.saveWishlist([]); 234 this.updateAllButtons(); 235 this.updateWishlistCount(); 236
237 if (this.wishlistPage) { 238 this.renderWishlistPage(); 239 } 240 } 241
242 showNotification(message) { 243 const notification = document.createElement('div'); 244 notification.className = 'wishlist-notification'; 245 notification.textContent = message; 246
247 document.body.appendChild(notification); 248
249 setTimeout(() => { 250 notification.classList.add('show'); 251 }, 10); 252
253 setTimeout(() => { 254 notification.classList.remove('show'); 255 setTimeout(() => notification.remove(), 300); 256 }, 3000); 257 } 258
259 formatPrice(cents) { 260 return '$' + (cents / 100).toFixed(2); 261 } 262 } 263
264 // Initialize 265 document.addEventListener('DOMContentLoaded', () => { 266 window.wishlist = new Wishlist(); 267 });
Shopify Wishlist with LocalStorage
Create a complete wishlist feature that allows customers to save their favorite products for later, stored in localStorage for persistence across sessions.
// Wishlist Class
class Wishlist {
constructor(options = {}) {
this.storageKey = options.storageKey || 'shopify_wishlist';
this.wishlistPage = document.querySelector(options.wishlistPage || '[data-wishlist-container]');
this.init();
}
init() {
// Bind add to wishlist buttons
document.addEventListener('click', (e) => {
if (e.target.matches('[data-wishlist-add]') || e.target.closest('[data-wishlist-add]')) {
e.preventDefault();
const btn = e.target.closest('[data-wishlist-add]') || e.target;
this.toggleWishlist(btn);
}
});
// Update all wishlist buttons
this.updateAllButtons();
// Update wishlist count
this.updateWishlistCount();
// Render wishlist page if on wishlist page
if (this.wishlistPage) {
this.renderWishlistPage();
}
}
toggleWishlist(button) {
const productId = button.dataset.wishlistAdd;
const productHandle = button.dataset.productHandle;
const productTitle = button.dataset.productTitle;
const productImage = button.dataset.productImage;
const productPrice = button.dataset.productPrice;
if (!productId) return;
const wishlist = this.getWishlist();
const index = wishlist.findIndex(item => item.id === productId);
if (index > -1) {
// Remove from wishlist
wishlist.splice(index, 1);
this.showNotification(`${productTitle} removed from wishlist`);
} else {
// Add to wishlist
wishlist.push({
id: productId,
handle: productHandle,
title: productTitle,
image: productImage,
price: productPrice,
addedAt: Date.now()
});
this.showNotification(`${productTitle} added to wishlist`);
}
this.saveWishlist(wishlist);
this.updateAllButtons();
this.updateWishlistCount();
// Refresh wishlist page if open
if (this.wishlistPage) {
this.renderWishlistPage();
}
}
getWishlist() {
try {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : [];
} catch (error) {
console.error('Error reading wishlist:', error);
return [];
}
}
saveWishlist(wishlist) {
try {
localStorage.setItem(this.storageKey, JSON.stringify(wishlist));
// Trigger custom event
document.dispatchEvent(new CustomEvent('wishlist:updated', {
detail: { wishlist }
}));
} catch (error) {
console.error('Error saving wishlist:', error);
}
}
isInWishlist(productId) {
const wishlist = this.getWishlist();
return wishlist.some(item => item.id === productId);
}
updateAllButtons() {
const buttons = document.querySelectorAll('[data-wishlist-add]');
buttons.forEach(button => {
const productId = button.dataset.wishlistAdd;
const isInWishlist = this.isInWishlist(productId);
if (isInWishlist) {
button.classList.add('in-wishlist');
button.setAttribute('aria-label', 'Remove from wishlist');
} else {
button.classList.remove('in-wishlist');
button.setAttribute('aria-label', 'Add to wishlist');
}
});
}
updateWishlistCount() {
const wishlist = this.getWishlist();
const countElements = document.querySelectorAll('[data-wishlist-count]');
countElements.forEach(el => {
el.textContent = wishlist.length;
el.style.display = wishlist.length > 0 ? 'inline' : 'none';
});
}
async renderWishlistPage() {
const wishlist = this.getWishlist();
if (wishlist.length === 0) {
this.wishlistPage.innerHTML = this.getEmptyTemplate();
return;
}
// Fetch fresh product data
const products = await this.fetchProducts(wishlist.map(item => item.handle));
this.wishlistPage.innerHTML = this.getWishlistTemplate(products);
// Bind remove buttons
this.wishlistPage.querySelectorAll('[data-remove-wishlist]').forEach(btn => {
btn.addEventListener('click', () => {
const productId = btn.dataset.removeWishlist;
this.removeFromWishlist(productId);
});
});
}
async fetchProducts(handles) {
const products = [];
for (const handle of handles) {
try {
const response = await fetch(`/products/${handle}.js`);
const product = await response.json();
products.push(product);
} catch (error) {
console.error(`Error fetching product ${handle}:`, error);
}
}
return products;
}
getWishlistTemplate(products) {
return `
<div class="wishlist-header">
<h1>My Wishlist</h1>
<p>${products.length} item${products.length !== 1 ? 's' : ''}</p>
</div>
<div class="wishlist-grid">
${products.map(product => `
<div class="wishlist-item">
<button
class="wishlist-remove"
data-remove-wishlist="${product.id}"
aria-label="Remove from wishlist">
×
</button>
<a href="/products/${product.handle}" class="wishlist-item-link">
<div class="wishlist-item-image">
<img src="${product.featured_image}" alt="${product.title}">
</div>
<div class="wishlist-item-info">
<h3 class="wishlist-item-title">${product.title}</h3>
<div class="wishlist-item-price">
${this.formatPrice(product.price)}
</div>
${product.available ?
`<button class="btn btn--add-to-cart" onclick="addToCart(${product.variants[0].id}); return false;">
Add to Cart
</button>` :
`<p class="sold-out">Sold Out</p>`
}
</div>
</a>
</div>
`).join('')}
</div>
`;
}
getEmptyTemplate() {
return `
<div class="wishlist-empty">
<svg width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
<h2>Your Wishlist is Empty</h2>
<p>Save your favorite products to your wishlist</p>
<a href="/collections/all" class="btn">Continue Shopping</a>
</div>
`;
}
removeFromWishlist(productId) {
const wishlist = this.getWishlist();
const filtered = wishlist.filter(item => item.id !== productId);
this.saveWishlist(filtered);
this.updateAllButtons();
this.updateWishlistCount();
this.renderWishlistPage();
}
clearWishlist() {
this.saveWishlist([]);
this.updateAllButtons();
this.updateWishlistCount();
if (this.wishlistPage) {
this.renderWishlistPage();
}
}
showNotification(message) {
const notification = document.createElement('div');
notification.className = 'wishlist-notification';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('show');
}, 10);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
formatPrice(cents) {
return '$' + (cents / 100).toFixed(2);
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
window.wishlist = new Wishlist();
});
Liquid Template - Wishlist Button
<!-- Add to product cards -->
<button
type="button"
class="wishlist-button"
data-wishlist-add="{{ product.id }}"
data-product-handle="{{ product.handle }}"
data-product-title="{{ product.title }}"
data-product-image="{{ product.featured_image | img_url: 'medium' }}"
data-product-price="{{ product.price }}"
aria-label="Add to wishlist">
<svg class="heart-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
</button>
Liquid Template - Wishlist Page
<!-- Create page template: templates/page.wishlist.liquid -->
<div class="wishlist-page">
<div class="container">
<div data-wishlist-container>
<!-- Content will be rendered by JavaScript -->
</div>
</div>
</div>
Liquid Template - Header Link
<!-- Add to header -->
<a href="/pages/wishlist" class="wishlist-link">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
<span class="wishlist-count" data-wishlist-count>0</span>
</a>
CSS Styling
/* Wishlist Button */
.wishlist-button {
background: none;
border: none;
cursor: pointer;
padding: 8px;
transition: transform 0.2s;
}
.wishlist-button:hover {
transform: scale(1.1);
}
.wishlist-button .heart-icon {
stroke: #000;
fill: none;
transition: fill 0.3s;
}
.wishlist-button.in-wishlist .heart-icon {
fill: #e74c3c;
stroke: #e74c3c;
}
/* Wishlist Page */
.wishlist-header {
text-align: center;
margin-bottom: 40px;
}
.wishlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 30px;
}
.wishlist-item {
position: relative;
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.3s;
}
.wishlist-item:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.wishlist-remove {
position: absolute;
top: 10px;
right: 10px;
width: 30px;
height: 30px;
background: #fff;
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 24px;
line-height: 1;
z-index: 2;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.wishlist-item-link {
display: block;
text-decoration: none;
color: inherit;
}
.wishlist-item-image {
position: relative;
padding-bottom: 100%;
overflow: hidden;
}
.wishlist-item-image img {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}
.wishlist-item-info {
padding: 15px;
}
.wishlist-item-title {
font-size: 16px;
margin: 0 0 10px;
}
.wishlist-item-price {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
}
.btn--add-to-cart {
width: 100%;
padding: 10px;
background: #000;
color: #fff;
border: none;
cursor: pointer;
}
/* Empty State */
.wishlist-empty {
text-align: center;
padding: 100px 20px;
}
.wishlist-empty svg {
color: #ccc;
margin-bottom: 20px;
}
/* Notification */
.wishlist-notification {
position: fixed;
bottom: 20px;
right: 20px;
background: #000;
color: #fff;
padding: 15px 20px;
border-radius: 6px;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease;
z-index: 10000;
}
.wishlist-notification.show {
opacity: 1;
transform: translateY(0);
}
Features
- No App Required: Pure JavaScript solution
- Persistent Storage: Uses localStorage for persistence
- Full CRUD: Add, view, remove products
- Product Count: Shows number of wishlist items
- Fresh Data: Fetches current product info on display
- Visual Feedback: Notifications and button states
- Empty State: Friendly message when wishlist is empty
- Mobile Friendly: Responsive grid layout
- Event System: Custom events for integration
Related Snippets
Shopify Recently Viewed Products
Track and display products customers have recently viewed
JAVASCRIPTshopifybeginner
javascriptPreview
// Recently Viewed Products Class
class RecentlyViewed {
constructor(options = {}) {
this.storageKey = options.storageKey || 'recently_viewed_products';
...#shopify#recently-viewed#localstorage+1
11/26/2025
View
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 Free Shipping Progress Bar
Show customers how much more they need to spend for free shipping
JAVASCRIPTshopifybeginner
javascriptPreview
// Free Shipping Bar Class
class FreeShippingBar {
constructor(options = {}) {
this.threshold = options.threshold || 5000; // Amount in cents
...#shopify#shipping#cart+2
11/25/2025
View