JAVASCRIPTshopifyintermediate

Shopify Back in Stock Notifications

Let customers subscribe to email notifications when out-of-stock products are available

#shopify#inventory#notifications#email#stock-alerts
Share this snippet:

Code

javascript
1// Back in Stock Notification Class
2class BackInStockNotifier {
3 constructor(options = {}) {
4 this.productId = options.productId;
5 this.variantId = options.variantId;
6 this.button = document.querySelector(options.button || '[data-notify-button]');
7 this.modal = document.querySelector(options.modal || '[data-notify-modal]');
8 this.form = document.querySelector(options.form || '[data-notify-form]');
9
10 if (!this.button) return;
11
12 this.init();
13 }
14
15 init() {
16 // Show modal on button click
17 this.button.addEventListener('click', () => this.showModal());
18
19 // Handle form submission
20 if (this.form) {
21 this.form.addEventListener('submit', (e) => this.handleSubmit(e));
22 }
23
24 // Close modal
25 const closeBtn = this.modal?.querySelector('[data-close-modal]');
26 if (closeBtn) {
27 closeBtn.addEventListener('click', () => this.hideModal());
28 }
29
30 // Close on overlay click
31 this.modal?.addEventListener('click', (e) => {
32 if (e.target === this.modal) {
33 this.hideModal();
34 }
35 });
36 }
37
38 showModal() {
39 if (!this.modal) {
40 this.createModal();
41 }
42
43 this.modal.classList.add('is-active');
44 document.body.style.overflow = 'hidden';
45 }
46
47 hideModal() {
48 this.modal.classList.remove('is-active');
49 document.body.style.overflow = '';
50 }
51
52 createModal() {
53 this.modal = document.createElement('div');
54 this.modal.className = 'notify-modal';
55 this.modal.setAttribute('data-notify-modal', '');
56
57 this.modal.innerHTML = `
58 <div class="notify-modal-content">
59 <button class="notify-close" data-close-modal>×</button>
60
61 <h3>Notify Me When Available</h3>
62 <p>Enter your email and we'll notify you when this product is back in stock.</p>
63
64 <form data-notify-form class="notify-form">
65 <input
66 type="email"
67 name="email"
68 placeholder="your@email.com"
69 required
70 class="notify-input">
71
72 <input type="hidden" name="product_id" value="${this.productId}">
73 <input type="hidden" name="variant_id" value="${this.variantId}">
74
75 <button type="submit" class="btn btn--notify">
76 Notify Me
77 </button>
78 </form>
79
80 <div class="notify-success" style="display: none;">
81 <p>✓ You'll be notified when this item is back in stock!</p>
82 </div>
83
84 <p class="notify-privacy">
85 We respect your privacy. Unsubscribe at any time.
86 </p>
87 </div>
88 `;
89
90 document.body.appendChild(this.modal);
91
92 this.form = this.modal.querySelector('[data-notify-form]');
93 this.form.addEventListener('submit', (e) => this.handleSubmit(e));
94
95 this.modal.addEventListener('click', (e) => {
96 if (e.target === this.modal) {
97 this.hideModal();
98 }
99 });
100 }
101
102 async handleSubmit(e) {
103 e.preventDefault();
104
105 const formData = new FormData(this.form);
106 const email = formData.get('email');
107 const productId = formData.get('product_id');
108 const variantId = formData.get('variant_id');
109
110 try {
111 // Store notification request (requires backend/app)
112 const response = await this.saveNotificationRequest(email, productId, variantId);
113
114 if (response.ok) {
115 this.showSuccess();
116 } else {
117 this.showError();
118 }
119 } catch (error) {
120 console.error('Notification signup error:', error);
121 this.showError();
122 }
123 }
124
125 async saveNotificationRequest(email, productId, variantId) {
126 // Option 1: Use a Shopify app endpoint
127 // Most back-in-stock apps provide an API endpoint
128
129 // Option 2: Save to your own backend
130 return await fetch('/apps/back-in-stock/subscribe', {
131 method: 'POST',
132 headers: {
133 'Content-Type': 'application/json',
134 },
135 body: JSON.stringify({
136 email,
137 product_id: productId,
138 variant_id: variantId,
139 shop: window.Shopify.shop
140 })
141 });
142
143 // Option 3: Use customer metafields (Shopify Plus)
144 // Store in customer metafield
145
146 // Option 4: Email to store owner (simple fallback)
147 /*
148 return await fetch('/contact', {
149 method: 'POST',
150 headers: {
151 'Content-Type': 'application/json',
152 },
153 body: JSON.stringify({
154 contact: {
155 email: email,
156 body: `Back in stock notification request for product ${productId}, variant ${variantId}`
157 }
158 })
159 });
160 */
161 }
162
163 showSuccess() {
164 this.form.style.display = 'none';
165 const success = this.modal.querySelector('.notify-success');
166 if (success) {
167 success.style.display = 'block';
168 }
169
170 // Auto-close after 3 seconds
171 setTimeout(() => {
172 this.hideModal();
173 this.resetForm();
174 }, 3000);
175 }
176
177 showError() {
178 alert('Something went wrong. Please try again.');
179 }
180
181 resetForm() {
182 this.form.reset();
183 this.form.style.display = 'block';
184 const success = this.modal.querySelector('.notify-success');
185 if (success) {
186 success.style.display = 'none';
187 }
188 }
189}
190
191// Auto-initialize for sold out products
192document.addEventListener('DOMContentLoaded', () => {
193 // Check if product is sold out
194 const addToCartBtn = document.querySelector('[data-add-to-cart]');
195 const notifyBtn = document.querySelector('[data-notify-button]');
196
197 if (addToCartBtn && notifyBtn) {
198 const isSoldOut = addToCartBtn.disabled || addToCartBtn.textContent.includes('Sold Out');
199
200 if (isSoldOut) {
201 addToCartBtn.style.display = 'none';
202 notifyBtn.style.display = 'block';
203
204 const productId = notifyBtn.dataset.productId;
205 const variantId = notifyBtn.dataset.variantId;
206
207 new BackInStockNotifier({
208 productId,
209 variantId
210 });
211 }
212 }
213});

Shopify Back in Stock Notifications

Allow customers to sign up for email notifications when out-of-stock products become available again, helping you capture lost sales and build your email list.

// Back in Stock Notification Class
class BackInStockNotifier {
    constructor(options = {}) {
        this.productId = options.productId;
        this.variantId = options.variantId;
        this.button = document.querySelector(options.button || '[data-notify-button]');
        this.modal = document.querySelector(options.modal || '[data-notify-modal]');
        this.form = document.querySelector(options.form || '[data-notify-form]');

        if (!this.button) return;

        this.init();
    }

    init() {
        // Show modal on button click
        this.button.addEventListener('click', () => this.showModal());

        // Handle form submission
        if (this.form) {
            this.form.addEventListener('submit', (e) => this.handleSubmit(e));
        }

        // Close modal
        const closeBtn = this.modal?.querySelector('[data-close-modal]');
        if (closeBtn) {
            closeBtn.addEventListener('click', () => this.hideModal());
        }

        // Close on overlay click
        this.modal?.addEventListener('click', (e) => {
            if (e.target === this.modal) {
                this.hideModal();
            }
        });
    }

    showModal() {
        if (!this.modal) {
            this.createModal();
        }

        this.modal.classList.add('is-active');
        document.body.style.overflow = 'hidden';
    }

    hideModal() {
        this.modal.classList.remove('is-active');
        document.body.style.overflow = '';
    }

    createModal() {
        this.modal = document.createElement('div');
        this.modal.className = 'notify-modal';
        this.modal.setAttribute('data-notify-modal', '');

        this.modal.innerHTML = `
            <div class="notify-modal-content">
                <button class="notify-close" data-close-modal>×</button>

                <h3>Notify Me When Available</h3>
                <p>Enter your email and we'll notify you when this product is back in stock.</p>

                <form data-notify-form class="notify-form">
                    <input
                        type="email"
                        name="email"
                        placeholder="your@email.com"
                        required
                        class="notify-input">

                    <input type="hidden" name="product_id" value="${this.productId}">
                    <input type="hidden" name="variant_id" value="${this.variantId}">

                    <button type="submit" class="btn btn--notify">
                        Notify Me
                    </button>
                </form>

                <div class="notify-success" style="display: none;">
                    <p>✓ You'll be notified when this item is back in stock!</p>
                </div>

                <p class="notify-privacy">
                    We respect your privacy. Unsubscribe at any time.
                </p>
            </div>
        `;

        document.body.appendChild(this.modal);

        this.form = this.modal.querySelector('[data-notify-form]');
        this.form.addEventListener('submit', (e) => this.handleSubmit(e));

        this.modal.addEventListener('click', (e) => {
            if (e.target === this.modal) {
                this.hideModal();
            }
        });
    }

    async handleSubmit(e) {
        e.preventDefault();

        const formData = new FormData(this.form);
        const email = formData.get('email');
        const productId = formData.get('product_id');
        const variantId = formData.get('variant_id');

        try {
            // Store notification request (requires backend/app)
            const response = await this.saveNotificationRequest(email, productId, variantId);

            if (response.ok) {
                this.showSuccess();
            } else {
                this.showError();
            }
        } catch (error) {
            console.error('Notification signup error:', error);
            this.showError();
        }
    }

    async saveNotificationRequest(email, productId, variantId) {
        // Option 1: Use a Shopify app endpoint
        // Most back-in-stock apps provide an API endpoint

        // Option 2: Save to your own backend
        return await fetch('/apps/back-in-stock/subscribe', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                email,
                product_id: productId,
                variant_id: variantId,
                shop: window.Shopify.shop
            })
        });

        // Option 3: Use customer metafields (Shopify Plus)
        // Store in customer metafield

        // Option 4: Email to store owner (simple fallback)
        /*
        return await fetch('/contact', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                contact: {
                    email: email,
                    body: `Back in stock notification request for product ${productId}, variant ${variantId}`
                }
            })
        });
        */
    }

    showSuccess() {
        this.form.style.display = 'none';
        const success = this.modal.querySelector('.notify-success');
        if (success) {
            success.style.display = 'block';
        }

        // Auto-close after 3 seconds
        setTimeout(() => {
            this.hideModal();
            this.resetForm();
        }, 3000);
    }

    showError() {
        alert('Something went wrong. Please try again.');
    }

    resetForm() {
        this.form.reset();
        this.form.style.display = 'block';
        const success = this.modal.querySelector('.notify-success');
        if (success) {
            success.style.display = 'none';
        }
    }
}

// Auto-initialize for sold out products
document.addEventListener('DOMContentLoaded', () => {
    // Check if product is sold out
    const addToCartBtn = document.querySelector('[data-add-to-cart]');
    const notifyBtn = document.querySelector('[data-notify-button]');

    if (addToCartBtn && notifyBtn) {
        const isSoldOut = addToCartBtn.disabled || addToCartBtn.textContent.includes('Sold Out');

        if (isSoldOut) {
            addToCartBtn.style.display = 'none';
            notifyBtn.style.display = 'block';

            const productId = notifyBtn.dataset.productId;
            const variantId = notifyBtn.dataset.variantId;

            new BackInStockNotifier({
                productId,
                variantId
            });
        }
    }
});

Liquid Template

<!-- Product Form with Notify Button -->
<div class="product-actions">
    {% if product.available %}
        <button
            type="submit"
            name="add"
            data-add-to-cart
            class="btn btn--add-to-cart">
            Add to Cart
        </button>
    {% else %}
        <button
            type="button"
            data-notify-button
            data-product-id="{{ product.id }}"
            data-variant-id="{{ product.selected_or_first_available_variant.id }}"
            class="btn btn--notify"
            style="display: none;">
            Notify Me When Available
        </button>
    {% endif %}
</div>

<!-- Variant change handling -->
<script>
document.addEventListener('DOMContentLoaded', () => {
    const variantSelect = document.querySelector('[data-variant-select]');
    const addToCartBtn = document.querySelector('[data-add-to-cart]');
    const notifyBtn = document.querySelector('[data-notify-button]');

    if (variantSelect && addToCartBtn && notifyBtn) {
        variantSelect.addEventListener('change', (e) => {
            const selectedOption = e.target.options[e.target.selectedIndex];
            const available = selectedOption.dataset.available === 'true';
            const variantId = selectedOption.value;

            if (available) {
                addToCartBtn.style.display = 'block';
                addToCartBtn.disabled = false;
                notifyBtn.style.display = 'none';
            } else {
                addToCartBtn.style.display = 'none';
                notifyBtn.style.display = 'block';
                notifyBtn.dataset.variantId = variantId;
            }
        });
    }
});
</script>

CSS Styling

/* Notify Button */
.btn--notify {
    width: 100%;
    padding: 15px 30px;
    background: #0066cc;
    color: #fff;
    border: none;
    border-radius: 4px;
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.3s;
}

.btn--notify:hover {
    background: #0052a3;
}

/* Modal */
.notify-modal {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0,0,0,0.7);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 9999;
    opacity: 0;
    visibility: hidden;
    transition: all 0.3s ease;
}

.notify-modal.is-active {
    opacity: 1;
    visibility: visible;
}

.notify-modal-content {
    background: #fff;
    border-radius: 8px;
    padding: 40px;
    max-width: 500px;
    width: 90%;
    position: relative;
    transform: scale(0.9);
    transition: transform 0.3s ease;
}

.notify-modal.is-active .notify-modal-content {
    transform: scale(1);
}

.notify-close {
    position: absolute;
    top: 15px;
    right: 15px;
    background: none;
    border: none;
    font-size: 30px;
    cursor: pointer;
    line-height: 1;
    padding: 5px;
}

.notify-modal-content h3 {
    margin-bottom: 10px;
    font-size: 24px;
}

.notify-modal-content > p {
    margin-bottom: 25px;
    color: #666;
}

/* Form */
.notify-form {
    margin-bottom: 20px;
}

.notify-input {
    width: 100%;
    padding: 12px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 16px;
    margin-bottom: 15px;
}

.notify-input:focus {
    outline: none;
    border-color: #0066cc;
}

.btn--notify {
    width: 100%;
}

/* Success Message */
.notify-success {
    padding: 20px;
    background: #d4edda;
    border: 1px solid #c3e6cb;
    border-radius: 4px;
    color: #155724;
    text-align: center;
    margin-bottom: 20px;
}

/* Privacy Note */
.notify-privacy {
    font-size: 12px;
    color: #999;
    text-align: center;
    margin: 0;
}

Integration with Klaviyo

// Use Klaviyo for back-in-stock emails
async saveNotificationRequest(email, productId, variantId) {
    const klaviyoListId = 'YOUR_KLAVIYO_LIST_ID';

    return await fetch('https://a.klaviyo.com/api/v2/list/' + klaviyoListId + '/subscribe', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            api_key: 'YOUR_KLAVIYO_PUBLIC_API_KEY',
            profiles: [{
                email: email,
                $consent: 'email',
                properties: {
                    notify_product_id: productId,
                    notify_variant_id: variantId,
                }
            }]
        })
    });
}

Server-Side Notification Handler (Example)

// Example backend endpoint to handle notifications
// This would run on your server or Shopify app

async function sendBackInStockEmail(productId, variantId) {
    // Get all subscribers for this product/variant
    const subscribers = await getSubscribers(productId, variantId);

    // Get product details
    const product = await shopify.product.get(productId);
    const variant = product.variants.find(v => v.id == variantId);

    // Send emails
    for (const subscriber of subscribers) {
        await sendEmail({
            to: subscriber.email,
            subject: `${product.title} is back in stock!`,
            template: 'back-in-stock',
            data: {
                product_title: product.title,
                product_url: `https://yourstore.com/products/${product.handle}`,
                variant_title: variant.title,
                product_image: product.images[0]?.src
            }
        });

        // Mark as notified
        await markAsNotified(subscriber.id);
    }
}

Features

  • Email Capture: Build email list from out-of-stock products
  • Variant Support: Track specific variants
  • Modal Interface: Clean, professional popup
  • Privacy Focused: Clear privacy messaging
  • Auto-Detection: Shows automatically for sold-out items
  • Success Feedback: Visual confirmation of subscription
  • Multiple Integrations: Works with Klaviyo, custom backends, apps
  • Responsive: Mobile-friendly design

Related Snippets