JAVASCRIPTshopifybeginner

Shopify Sticky Add to Cart Bar

Keep the add to cart button visible as users scroll down product pages

#shopify#sticky#add-to-cart#scroll#conversion
Share this snippet:

Code

javascript
1// Sticky Add to Cart Class
2class StickyAddToCart {
3 constructor(options = {}) {
4 this.mainButton = document.querySelector(options.mainButton || '[data-main-add-to-cart]');
5 this.stickyBar = document.querySelector(options.stickyBar || '[data-sticky-cart]');
6 this.threshold = options.threshold || 100;
7
8 if (!this.mainButton || !this.stickyBar) {
9 console.error('Required elements not found');
10 return;
11 }
12
13 this.isVisible = false;
14 this.init();
15 }
16
17 init() {
18 // Watch scroll
19 window.addEventListener('scroll', () => this.handleScroll(), { passive: true });
20
21 // Sync quantity and variant selection
22 this.syncFormData();
23
24 // Handle sticky bar add to cart
25 this.handleStickySubmit();
26
27 // Initial check
28 this.handleScroll();
29 }
30
31 handleScroll() {
32 const mainButtonRect = this.mainButton.getBoundingClientRect();
33 const mainButtonVisible = mainButtonRect.top >= 0 && mainButtonRect.bottom <= window.innerHeight;
34
35 if (!mainButtonVisible && window.scrollY > this.threshold) {
36 this.show();
37 } else {
38 this.hide();
39 }
40 }
41
42 show() {
43 if (!this.isVisible) {
44 this.stickyBar.classList.add('is-visible');
45 this.isVisible = true;
46 }
47 }
48
49 hide() {
50 if (this.isVisible) {
51 this.stickyBar.classList.remove('is-visible');
52 this.isVisible = false;
53 }
54 }
55
56 syncFormData() {
57 // Sync variant selection
58 const mainVariantSelect = document.querySelector('[name="id"]');
59 const stickyVariantSelect = this.stickyBar.querySelector('[name="id"]');
60
61 if (mainVariantSelect && stickyVariantSelect) {
62 mainVariantSelect.addEventListener('change', (e) => {
63 stickyVariantSelect.value = e.target.value;
64 this.updateStickyPrice();
65 });
66 }
67
68 // Sync quantity
69 const mainQuantity = document.querySelector('[data-main-quantity]');
70 const stickyQuantity = this.stickyBar.querySelector('[data-sticky-quantity]');
71
72 if (mainQuantity && stickyQuantity) {
73 mainQuantity.addEventListener('change', (e) => {
74 stickyQuantity.value = e.target.value;
75 });
76
77 stickyQuantity.addEventListener('change', (e) => {
78 mainQuantity.value = e.target.value;
79 });
80 }
81 }
82
83 handleStickySubmit() {
84 const form = this.stickyBar.querySelector('form');
85
86 if (!form) return;
87
88 form.addEventListener('submit', async (e) => {
89 e.preventDefault();
90
91 const formData = new FormData(form);
92 const data = {
93 items: [{
94 id: formData.get('id'),
95 quantity: parseInt(formData.get('quantity') || 1)
96 }]
97 };
98
99 try {
100 const response = await fetch('/cart/add.js', {
101 method: 'POST',
102 headers: {
103 'Content-Type': 'application/json',
104 },
105 body: JSON.stringify(data)
106 });
107
108 if (response.ok) {
109 this.showSuccess();
110 // Trigger cart update
111 document.dispatchEvent(new CustomEvent('cart:updated'));
112 } else {
113 this.showError();
114 }
115 } catch (error) {
116 console.error('Add to cart error:', error);
117 this.showError();
118 }
119 });
120 }
121
122 updateStickyPrice() {
123 const variantSelect = this.stickyBar.querySelector('[name="id"]');
124 const priceElement = this.stickyBar.querySelector('[data-sticky-price]');
125
126 if (!variantSelect || !priceElement) return;
127
128 const selectedOption = variantSelect.options[variantSelect.selectedIndex];
129 const price = selectedOption.dataset.price;
130
131 if (price) {
132 priceElement.textContent = this.formatMoney(price);
133 }
134 }
135
136 showSuccess() {
137 const button = this.stickyBar.querySelector('button[type="submit"]');
138 const originalText = button.textContent;
139
140 button.textContent = 'Added! ✓';
141 button.classList.add('success');
142
143 setTimeout(() => {
144 button.textContent = originalText;
145 button.classList.remove('success');
146 }, 2000);
147 }
148
149 showError() {
150 const button = this.stickyBar.querySelector('button[type="submit"]');
151 const originalText = button.textContent;
152
153 button.textContent = 'Error!';
154 button.classList.add('error');
155
156 setTimeout(() => {
157 button.textContent = originalText;
158 button.classList.remove('error');
159 }, 2000);
160 }
161
162 formatMoney(cents) {
163 return '$' + (parseInt(cents) / 100).toFixed(2);
164 }
165}
166
167// Initialize
168document.addEventListener('DOMContentLoaded', () => {
169 new StickyAddToCart({
170 mainButton: '[data-main-add-to-cart]',
171 stickyBar: '[data-sticky-cart]',
172 threshold: 150
173 });
174});

Shopify Sticky Add to Cart Bar

Create a sticky add to cart bar that appears when the main add to cart button scrolls out of view, improving conversion rates especially on mobile devices.

// Sticky Add to Cart Class
class StickyAddToCart {
    constructor(options = {}) {
        this.mainButton = document.querySelector(options.mainButton || '[data-main-add-to-cart]');
        this.stickyBar = document.querySelector(options.stickyBar || '[data-sticky-cart]');
        this.threshold = options.threshold || 100;

        if (!this.mainButton || !this.stickyBar) {
            console.error('Required elements not found');
            return;
        }

        this.isVisible = false;
        this.init();
    }

    init() {
        // Watch scroll
        window.addEventListener('scroll', () => this.handleScroll(), { passive: true });

        // Sync quantity and variant selection
        this.syncFormData();

        // Handle sticky bar add to cart
        this.handleStickySubmit();

        // Initial check
        this.handleScroll();
    }

    handleScroll() {
        const mainButtonRect = this.mainButton.getBoundingClientRect();
        const mainButtonVisible = mainButtonRect.top >= 0 && mainButtonRect.bottom <= window.innerHeight;

        if (!mainButtonVisible && window.scrollY > this.threshold) {
            this.show();
        } else {
            this.hide();
        }
    }

    show() {
        if (!this.isVisible) {
            this.stickyBar.classList.add('is-visible');
            this.isVisible = true;
        }
    }

    hide() {
        if (this.isVisible) {
            this.stickyBar.classList.remove('is-visible');
            this.isVisible = false;
        }
    }

    syncFormData() {
        // Sync variant selection
        const mainVariantSelect = document.querySelector('[name="id"]');
        const stickyVariantSelect = this.stickyBar.querySelector('[name="id"]');

        if (mainVariantSelect && stickyVariantSelect) {
            mainVariantSelect.addEventListener('change', (e) => {
                stickyVariantSelect.value = e.target.value;
                this.updateStickyPrice();
            });
        }

        // Sync quantity
        const mainQuantity = document.querySelector('[data-main-quantity]');
        const stickyQuantity = this.stickyBar.querySelector('[data-sticky-quantity]');

        if (mainQuantity && stickyQuantity) {
            mainQuantity.addEventListener('change', (e) => {
                stickyQuantity.value = e.target.value;
            });

            stickyQuantity.addEventListener('change', (e) => {
                mainQuantity.value = e.target.value;
            });
        }
    }

    handleStickySubmit() {
        const form = this.stickyBar.querySelector('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') || 1)
                }]
            };

            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
                    document.dispatchEvent(new CustomEvent('cart:updated'));
                } else {
                    this.showError();
                }
            } catch (error) {
                console.error('Add to cart error:', error);
                this.showError();
            }
        });
    }

    updateStickyPrice() {
        const variantSelect = this.stickyBar.querySelector('[name="id"]');
        const priceElement = this.stickyBar.querySelector('[data-sticky-price]');

        if (!variantSelect || !priceElement) return;

        const selectedOption = variantSelect.options[variantSelect.selectedIndex];
        const price = selectedOption.dataset.price;

        if (price) {
            priceElement.textContent = this.formatMoney(price);
        }
    }

    showSuccess() {
        const button = this.stickyBar.querySelector('button[type="submit"]');
        const originalText = button.textContent;

        button.textContent = 'Added! ✓';
        button.classList.add('success');

        setTimeout(() => {
            button.textContent = originalText;
            button.classList.remove('success');
        }, 2000);
    }

    showError() {
        const button = this.stickyBar.querySelector('button[type="submit"]');
        const originalText = button.textContent;

        button.textContent = 'Error!';
        button.classList.add('error');

        setTimeout(() => {
            button.textContent = originalText;
            button.classList.remove('error');
        }, 2000);
    }

    formatMoney(cents) {
        return '$' + (parseInt(cents) / 100).toFixed(2);
    }
}

// Initialize
document.addEventListener('DOMContentLoaded', () => {
    new StickyAddToCart({
        mainButton: '[data-main-add-to-cart]',
        stickyBar: '[data-sticky-cart]',
        threshold: 150
    });
});

Liquid Template

<!-- Main Product Form -->
<form action="/cart/add" method="post" class="product-form">
    <select name="id" data-variant-select>
        {% for variant in product.variants %}
            <option
                value="{{ variant.id }}"
                data-price="{{ variant.price }}"
                {% unless variant.available %}disabled{% endunless %}>
                {{ variant.title }} - {{ variant.price | money }}
            </option>
        {% endfor %}
    </select>

    <input type="number" name="quantity" value="1" min="1" data-main-quantity>

    <button type="submit" data-main-add-to-cart class="btn btn--add-to-cart">
        Add to Cart
    </button>
</form>

<!-- Sticky Add to Cart Bar -->
<div class="sticky-cart-bar" data-sticky-cart>
    <div class="sticky-cart-content">
        <div class="sticky-cart-product">
            {% if product.featured_image %}
                <img src="{{ product.featured_image | img_url: '80x' }}" alt="{{ product.title }}">
            {% endif %}

            <div class="sticky-cart-info">
                <h4 class="sticky-cart-title">{{ product.title }}</h4>
                <div class="sticky-cart-price" data-sticky-price>
                    {{ product.price | money }}
                </div>
            </div>
        </div>

        <form action="/cart/add" method="post" class="sticky-cart-form">
            <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">

            <input
                type="number"
                name="quantity"
                value="1"
                min="1"
                class="sticky-quantity"
                data-sticky-quantity>

            <button type="submit" class="btn btn--sticky-cart">
                Add to Cart
            </button>
        </form>
    </div>
</div>

CSS Styling

/* Sticky Cart Bar */
.sticky-cart-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    background: #fff;
    border-top: 1px solid #e0e0e0;
    box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
    padding: 15px;
    transform: translateY(100%);
    transition: transform 0.3s ease;
    z-index: 999;
}

.sticky-cart-bar.is-visible {
    transform: translateY(0);
}

.sticky-cart-content {
    max-width: 1200px;
    margin: 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
}

.sticky-cart-product {
    display: flex;
    align-items: center;
    gap: 15px;
    flex: 1;
}

.sticky-cart-product img {
    width: 60px;
    height: 60px;
    object-fit: cover;
    border-radius: 4px;
}

.sticky-cart-info {
    flex: 1;
}

.sticky-cart-title {
    font-size: 14px;
    margin: 0 0 5px 0;
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sticky-cart-price {
    font-size: 16px;
    font-weight: bold;
}

.sticky-cart-form {
    display: flex;
    align-items: center;
    gap: 10px;
}

.sticky-quantity {
    width: 60px;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    text-align: center;
}

.btn--sticky-cart {
    padding: 12px 30px;
    background: #000;
    color: #fff;
    border: none;
    border-radius: 4px;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.3s;
}

.btn--sticky-cart:hover {
    background: #333;
}

.btn--sticky-cart.success {
    background: #4caf50;
}

.btn--sticky-cart.error {
    background: #f44336;
}

/* Mobile Responsive */
@media (max-width: 768px) {
    .sticky-cart-content {
        flex-wrap: wrap;
    }

    .sticky-cart-title {
        font-size: 13px;
    }

    .sticky-cart-price {
        font-size: 14px;
    }

    .btn--sticky-cart {
        padding: 10px 20px;
        font-size: 14px;
    }
}

@media (max-width: 576px) {
    .sticky-cart-bar {
        padding: 10px;
    }

    .sticky-cart-product img {
        width: 50px;
        height: 50px;
    }

    .sticky-quantity {
        width: 50px;
        padding: 8px;
    }

    .btn--sticky-cart {
        flex: 1;
    }
}

With Variant Selector

// Extended version with variant options in sticky bar
class StickyAddToCartAdvanced extends StickyAddToCart {
    init() {
        super.init();
        this.createVariantSelector();
    }

    createVariantSelector() {
        const mainSelect = document.querySelector('[data-variant-select]');
        const stickyForm = this.stickyBar.querySelector('.sticky-cart-form');

        if (!mainSelect || !stickyForm) return;

        // Clone variant select
        const stickySelect = mainSelect.cloneNode(true);
        stickySelect.classList.add('sticky-variant-select');

        // Insert before quantity
        const quantity = stickyForm.querySelector('[data-sticky-quantity]');
        stickyForm.insertBefore(stickySelect, quantity);

        // Sync selections
        mainSelect.addEventListener('change', (e) => {
            stickySelect.value = e.target.value;
            this.updateStickyPrice();
        });

        stickySelect.addEventListener('change', (e) => {
            mainSelect.value = e.target.value;
            this.updateStickyPrice();

            // Update hidden input
            const hiddenInput = stickyForm.querySelector('[name="id"]');
            if (hiddenInput) {
                hiddenInput.value = e.target.value;
            }
        });
    }
}

Features

  • Smart Detection: Shows only when main button is out of view
  • Synchronized: Keeps quantity and variants in sync
  • AJAX Add to Cart: No page reload when adding items
  • Visual Feedback: Success/error states
  • Mobile Optimized: Perfect for mobile shoppers
  • Product Info: Shows product image and price
  • Customizable: Easy to modify appearance and behavior
  • Performance: Passive scroll listeners for smooth scrolling

Related Snippets