JAVASCRIPTshopifyintermediate

Shopify Wishlist with LocalStorage

Add wishlist/favorites functionality without requiring a Shopify app

#shopify#wishlist#favorites#localstorage
Share this snippet:

Code

javascript
1// Wishlist Class
2class 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
265document.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>
<!-- 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