Custom Sticky Add To Cart Button not functioning

I created a sticky add to cart button following a guide by this guy here

I’ve used display: none; to hide a few elements in the Sticky Product Form leaving only the button.

I’ve also modified the sticky-product-form.js file with an intention to sync the sticky add to cart button with the regular one, partly because the original code given by the guy didn’t add the item to cart at all. Here is the modified version of the code below:

customElements.define(‘sticky-product-form’, class StickyProductForm extends HTMLElement {> > constructor() {> > super();> > }> > connectedCallback() {> > this.productForm = document.querySelector(‘product-form’);> > this.productStickyForm = document.querySelector(‘.product–sticky-form’);> > this.productFormBounds = {};> > this.onScrollHandler = this.onScroll.bind(this);> > window.addEventListener(‘scroll’, this.onScrollHandler, false);> > this.createObserver();> > // Sync variant and quantity inputs between forms> > this.syncInputs();> > }> > disconnectedCallback() {> > window.removeEventListener(‘scroll’, this.onScrollHandler);> > }> > createObserver() {> > let observer = new IntersectionObserver((entries, observer) => {> > this.productFormBounds = entries[0].isIntersecting;> > });> > observer.observe(this.productForm);> > }> > onScroll() {> > this.productFormBounds ? requestAnimationFrame(this.hide.bind(this)) : requestAnimationFrame(this.reveal.bind(this));> > }> > hide() {> > if (this.productStickyForm.classList.contains(‘product–sticky-form__active’)) {> > this.productStickyForm.classList.remove(‘product–sticky-form__active’);> > this.productStickyForm.classList.add(‘product–sticky-form__inactive’);> > }> > }> > reveal() {> > if (this.productStickyForm.classList.contains(‘product–sticky-form__inactive’)) {> > this.productStickyForm.classList.add(‘product–sticky-form__active’);> > this.productStickyForm.classList.remove(‘product–sticky-form__inactive’);> > }> > }> > syncInputs() {> > // Listen for changes on the main product form> > if (this.productForm) {> > this.productForm.addEventListener(‘change’, (event) => {> > const stickyFormInput = this.productStickyForm.querySelector(‘[name="’ + event.target.name + ‘"]’);> > if (stickyFormInput) {> > stickyFormInput.value = event.target.value;> > }> > });> > }> > // Listen for changes on the sticky product form> > if (this.productStickyForm) {> > this.productStickyForm.addEventListener(‘change’, (event) => {> > const mainFormInput = this.productForm.querySelector(‘[name="’ + event.target.name + ‘"]’);> > if (mainFormInput) {> > mainFormInput.value = event.target.value;> > }> > });> > }> > }> > });

But after I modified the code, it still doesn’t add the item to cart except it does sync the Sold Out status to the sticky button (not immediately, only after refreshing the page).

How can I fix this problem?jav

Hey @NguyenThiHoaCo ,

Thank you for sharing your sticky add-to-cart solution for Dawn themes. I’ve made some improvements to the JavaScript code to fix the cart functionality and enhance the overall user experience. The updated version includes proper form submission handling, better form synchronization, loading states, and error handling - all while maintaining the original sticky behavior you designed.

I’m attaching the improved JavaScript code that resolves the add-to-cart issues while keeping your excellent sticky form concept intact. Feel free to use it in your future projects or share it with others who might find it helpful.

customElements.define('sticky-product-form', class StickyProductForm extends HTMLElement {
  constructor() {
    super();
    this.mainForm = null;
    this.stickyForm = null;
    this.submitButton = null;
    this.errorMessageWrapper = null;
    this.errorMessage = null;
  }

  connectedCallback() {
    this.mainForm = document.querySelector('product-form form');
    this.stickyForm = this.querySelector('form');
    this.productForm = document.querySelector('product-form');
    this.productStickyForm = document.querySelector('.product--sticky-form');
    this.submitButton = this.querySelector('[type="submit"]');
    this.errorMessageWrapper = this.querySelector('.product-form__error-message-wrapper');
    this.errorMessage = this.querySelector('.product-form__error-message');
    
    this.productFormBounds = {};

    if (this.mainForm && this.stickyForm) {
      this.setupEventListeners();
    }

    this.onScrollHandler = this.onScroll.bind(this);
    window.addEventListener('scroll', this.onScrollHandler, false);
    this.createObserver();
  }

  disconnectedCallback() {
    window.removeEventListener('scroll', this.onScrollHandler);
  }

  setupEventListeners() {
    // Handle sticky form submission
    this.stickyForm.addEventListener('submit', this.onSubmitHandler.bind(this));

    // Sync variant selectors
    const variantSelects = this.stickyForm.querySelectorAll('select');
    variantSelects.forEach(select => {
      select.addEventListener('change', (event) => {
        const correspondingSelect = this.mainForm.querySelector(`select[name="${event.target.name}"]`);
        if (correspondingSelect) {
          correspondingSelect.value = event.target.value;
          correspondingSelect.dispatchEvent(new Event('change', { bubbles: true }));
        }
      });
    });

    // Sync quantity input
    const quantityInput = this.stickyForm.querySelector('[name="quantity"]');
    if (quantityInput) {
      quantityInput.addEventListener('change', (event) => {
        const mainQuantityInput = this.mainForm.querySelector('[name="quantity"]');
        if (mainQuantityInput) {
          mainQuantityInput.value = event.target.value;
          mainQuantityInput.dispatchEvent(new Event('change', { bubbles: true }));
        }
      });
    }

    // Sync from main form to sticky form
    this.mainForm.addEventListener('change', (event) => {
      const stickyInput = this.stickyForm.querySelector(`[name="${event.target.name}"]`);
      if (stickyInput) {
        stickyInput.value = event.target.value;
      }
    });

    // Handle quantity buttons
    const quantityButtons = this.querySelectorAll('.quantity__button');
    quantityButtons.forEach(button => {
      button.addEventListener('click', () => {
        const input = button.parentElement.querySelector('[name="quantity"]');
        const value = Number(input.value);
        const isPlus = button.name === 'plus';
        
        const newValue = isPlus ? value + 1 : value - 1;
        if (newValue >= 1) {
          input.value = newValue;
          input.dispatchEvent(new Event('change', { bubbles: true }));
        }
      });
    });
  }

  onSubmitHandler(evt) {
    evt.preventDefault();
    if (this.submitButton.classList.contains('loading')) return;

    this.handleErrorMessage();
    this.toggleLoadingState();

    const config = {
      method: 'POST',
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams(new FormData(this.stickyForm))
    };

    fetch(`${routes.cart_add_url}`, config)
      .then((response) => response.json())
      .then((response) => {
        if (response.status) {
          this.handleErrorMessage(response.description);
          return;
        }

        // Trigger cart update event
        this.dispatchEvent(new CustomEvent('cart:update', {
          bubbles: true,
          detail: {
            item: response
          }
        }));

        // Optional: Update cart drawer if it exists
        if (typeof window.updateCartDrawer === 'function') {
          window.updateCartDrawer();
        }
      })
      .catch((e) => {
        console.error(e);
        this.handleErrorMessage('An error occurred. Please try again.');
      })
      .finally(() => {
        this.toggleLoadingState(false);
      });
  }

  toggleLoadingState(isLoading = true) {
    if (this.submitButton) {
      this.submitButton.classList.toggle('loading', isLoading);
      this.submitButton.querySelector('.loading-overlay__spinner').classList.toggle('hidden', !isLoading);
    }
  }

  handleErrorMessage(errorMessage = false) {
    if (!this.errorMessageWrapper) return;
    
    this.errorMessageWrapper.toggleAttribute('hidden', !errorMessage);
    if (errorMessage) {
      this.errorMessage.textContent = errorMessage;
    }
  }

  createObserver() {
    let observer = new IntersectionObserver((entries, observer) => {
      this.productFormBounds = entries[0].isIntersecting;
    });
    observer.observe(this.productForm);
  }

  onScroll() {
    this.productFormBounds ? 
      requestAnimationFrame(this.hide.bind(this)) : 
      requestAnimationFrame(this.reveal.bind(this));
  }

  hide() {
    if (this.productStickyForm.classList.contains('product--sticky-form__active')) {
      this.productStickyForm.classList.remove('product--sticky-form__active');
      this.productStickyForm.classList.add('product--sticky-form__inactive');
    }
  }

  reveal() {
    if (this.productStickyForm.classList.contains('product--sticky-form__inactive')) {
      this.productStickyForm.classList.add('product--sticky-form__active');
      this.productStickyForm.classList.remove('product--sticky-form__inactive');
    }
  }
});

Feel free to email me, in case you’ve any more questions.

Cheers!
Shubham | Untechnickle

Hi thank you so much for your help. However, it seems like I still have the same problems. Nothing was affected after I updated mine to your code, I just don’t any changes made to the button.

There was a part in the guide that says put a certain series of code just above the product-modal tag. I couldn’t find that tag anywhere but I found the product-modal.js in the file and put that series of code above it.

Is this the cause of the problem I’m facing? I’d appreciate if you could point it out for me. Again thanks so much.

Hi @NguyenThiHoaCo ,

I understand the issue now - it’s about the installation location. Let me help you set this up correctly:

  1. First, you need to find the right file. You’re looking for the main-product.liquid file, NOT the product-modal.js. You can find this at:
    • sections/main-product.liquid
  2. In main-product.liquid, look for the closing HTML tag (not the JS file). You need to add this code ABOVE that tag:
{%- if section.settings.show_sticky_cart_button -%}
  {% render 'product-sticky-form', product: product %}
{%- endif -%}
  • Also in main-product.liquid, you need to add the settings option. Find the schema section (usually at the bottom of the file) and add this to the settings array:
{
  "type": "checkbox",
  "id": "show_sticky_cart_button",
  "label": "Show sticky add to cart button to the bottom of the page",
  "default": true
}​
  • Then create three new files:
    • snippets/product-sticky-form.liquid (with the HTML code)
    • assets/product-sticky-form.js (with the JavaScript code)
    • assets/product-sticky-form.css (with the CSS code)

Could you confirm if you’ve placed all the code in these exact locations? The JS code won’t work if these files aren’t in the correct places.

Also, after making these changes, make sure to:

  1. Save all files
  2. Clear your theme cache
  3. Refresh your store page

Let me know if you need our experts help setting this up. We’d do it for free, no sweat :slightly_smiling_face:

Best regards,
Shubham

Could you please test it on your side with the Dawn theme (newest version)? I can’t seem to find anywhere.

So the catch is that I didn’t put the code in the product-modal.js file but above the reference of product-modal.js within the main-product.liquid

Somehow that’s magically the only place that makes the button appear on my store website. But the function doesn’t 100% deliver as expected.

However, I did see tags like #productModal in the file but not product-modal or other than the link reference of product-modal.js in the main-product.liquid file.

Hi, could please help me again ?

Hey @NguyenThiHoaCo ,

You have to put that code above product-media-modal tag. Check the code below:

{%- if section.settings.show_sticky_cart_button-%}
    {% render 'product-sticky-form', product: product %}
{%- endif -%}

{% render 'product-media-modal', variant_images: variant_images %}

If this doesn’t work, then please send us your collaborator code in the DM and we’ll do it for free. That’s all I can offer.

Thank you,
Shubham | hello@untechnickle.com

The sticky button technically is fine by itself but the function fails to deliver. I think it’s something to do with the JavaScript?

customElements.define(‘sticky-product-form’, class StickyProductForm extends HTMLElement {

constructor() {

super();

this.mainForm = null;

this.stickyForm = null;

this.submitButton = null;

this.errorMessageWrapper = null;

this.errorMessage = null;

}

connectedCallback() {

this.mainForm = document.querySelector(‘product-form form’);

this.stickyForm = this.querySelector(‘form’);

this.productForm = document.querySelector(‘product-form’);

this.productStickyForm = document.querySelector(‘.product–sticky-form’);

this.submitButton = this.querySelector(‘[type=“submit”]’);

this.errorMessageWrapper = this.querySelector(‘.product-form__error-message-wrapper’);

this.errorMessage = this.querySelector(‘.product-form__error-message’);

this.productFormBounds = {};

if (this.mainForm && this.stickyForm) {

this.setupEventListeners();

}

this.onScrollHandler = this.onScroll.bind(this);

window.addEventListener(‘scroll’, this.onScrollHandler, false);

this.createObserver();

}

disconnectedCallback() {

window.removeEventListener(‘scroll’, this.onScrollHandler);

}

setupEventListeners() {

// Handle sticky form submission

this.stickyForm.addEventListener(‘submit’, this.onSubmitHandler.bind(this));

// Sync variant selectors

const variantSelects = this.stickyForm.querySelectorAll(‘select’);

variantSelects.forEach(select => {

select.addEventListener(‘change’, (event) => {

const correspondingSelect = this.mainForm.querySelector(select[name="${event.target.name}"]);

if (correspondingSelect) {

correspondingSelect.value = event.target.value;

correspondingSelect.dispatchEvent(new Event(‘change’, { bubbles: true }));

}

});

});

// Sync quantity input

const quantityInput = this.stickyForm.querySelector(‘[name=“quantity”]’);

if (quantityInput) {

quantityInput.addEventListener(‘change’, (event) => {

const mainQuantityInput = this.mainForm.querySelector(‘[name=“quantity”]’);

if (mainQuantityInput) {

mainQuantityInput.value = event.target.value;

mainQuantityInput.dispatchEvent(new Event(‘change’, { bubbles: true }));

}

});

}

// Sync from main form to sticky form

this.mainForm.addEventListener(‘change’, (event) => {

const stickyInput = this.stickyForm.querySelector([name="${event.target.name}"]);

if (stickyInput) {

stickyInput.value = event.target.value;

}

});

// Handle quantity buttons

const quantityButtons = this.querySelectorAll(‘.quantity__button’);

quantityButtons.forEach(button => {

button.addEventListener(‘click’, () => {

const input = button.parentElement.querySelector(‘[name=“quantity”]’);

const value = Number(input.value);

const isPlus = button.name === ‘plus’;

const newValue = isPlus ? value + 1 : value - 1;

if (newValue >= 1) {

input.value = newValue;

input.dispatchEvent(new Event(‘change’, { bubbles: true }));

}

});

});

}

onSubmitHandler(evt) {

evt.preventDefault();

if (this.submitButton.classList.contains(‘loading’)) return;

this.handleErrorMessage();

this.toggleLoadingState();

const config = {

method: ‘POST’,

headers: {

‘X-Requested-With’: ‘XMLHttpRequest’,

‘Content-Type’: ‘application/x-www-form-urlencoded’

},

body: new URLSearchParams(new FormData(this.stickyForm))

};

fetch(${routes.cart_add_url}, config)

.then((response) => response.json())

.then((response) => {

if (response.status) {

this.handleErrorMessage(response.description);

return;

}

// Trigger cart update event

this.dispatchEvent(new CustomEvent(‘cart:update’, {

bubbles: true,

detail: {

item: response

}

}));

// Optional: Update cart drawer if it exists

if (typeof window.updateCartDrawer === ‘function’) {

window.updateCartDrawer();

}

})

.catch((e) => {

console.error(e);

this.handleErrorMessage(‘An error occurred. Please try again.’);

})

.finally(() => {

this.toggleLoadingState(false);

});

}

toggleLoadingState(isLoading = true) {

if (this.submitButton) {

this.submitButton.classList.toggle(‘loading’, isLoading);

this.submitButton.querySelector(‘.loading-overlay__spinner’).classList.toggle(‘hidden’, !isLoading);

}

}

handleErrorMessage(errorMessage = false) {

if (!this.errorMessageWrapper) return;

this.errorMessageWrapper.toggleAttribute(‘hidden’, !errorMessage);

if (errorMessage) {

this.errorMessage.textContent = errorMessage;

}

}

createObserver() {

let observer = new IntersectionObserver((entries, observer) => {

this.productFormBounds = entries[0].isIntersecting;

});

observer.observe(this.productForm);

}

onScroll() {

this.productFormBounds ?

requestAnimationFrame(this.hide.bind(this)) :

requestAnimationFrame(this.reveal.bind(this));

}

hide() {

if (this.productStickyForm.classList.contains(‘product–sticky-form__active’)) {

this.productStickyForm.classList.remove(‘product–sticky-form__active’);

this.productStickyForm.classList.add(‘product–sticky-form__inactive’);

}

}

reveal() {

if (this.productStickyForm.classList.contains(‘product–sticky-form__inactive’)) {

this.productStickyForm.classList.add(‘product–sticky-form__active’);

this.productStickyForm.classList.remove(‘product–sticky-form__inactive’);

}

}

});

Two things:

  1. Replace your entire product-sticky-form.js content with this new code. I’ve optimised it a bit.
customElements.define('sticky-product-form', class StickyProductForm extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    // Get main form elements
    this.mainProductForm = document.querySelector('product-form');
    this.mainForm = document.querySelector('product-form form');
    
    // Get sticky form elements
    this.stickyForm = this.querySelector('form');
    this.productStickyForm = document.querySelector('.product--sticky-form');
    this.submitButton = this.querySelector('[type="submit"]');
    
    // Error elements
    this.errorMessageWrapper = this.querySelector('.product-form__error-message-wrapper');
    this.errorMessage = this.querySelector('.product-form__error-message');
    
    this.productFormBounds = {};
    this.cartNotification = document.querySelector('cart-notification') || document.querySelector('#cart-notification');

    if (this.mainForm && this.stickyForm) {
      this.setupEventListeners();
    }

    this.onScrollHandler = this.onScroll.bind(this);
    window.addEventListener('scroll', this.onScrollHandler, false);
    this.createObserver();
  }

  disconnectedCallback() {
    window.removeEventListener('scroll', this.onScrollHandler);
  }

  setupEventListeners() {
    // Handle sticky form submission
    if (this.stickyForm) {
      this.stickyForm.addEventListener('submit', this.onSubmitHandler.bind(this));
    }

    // Sync main form to sticky form
    if (this.mainForm) {
      this.mainForm.addEventListener('change', (event) => {
        const stickyInput = this.stickyForm.querySelector(`[name="${event.target.name}"]`);
        if (stickyInput && stickyInput.value !== event.target.value) {
          stickyInput.value = event.target.value;
          
          // Handle variant changes
          if (event.target.name.includes('options[')) {
            const variantId = this.mainForm.querySelector('[name="id"]').value;
            const stickyVariantInput = this.stickyForm.querySelector('[name="id"]');
            if (stickyVariantInput) {
              stickyVariantInput.value = variantId;
              stickyVariantInput.disabled = false;
            }
          }
        }
      });
    }

    // Handle quantity buttons
    const quantityButtons = this.querySelectorAll('button[name="plus"], button[name="minus"]');
    quantityButtons.forEach(button => {
      button.addEventListener('click', () => {
        const input = button.parentElement.querySelector('[name="quantity"]');
        const currentValue = parseInt(input.value);
        const isPlus = button.name === 'plus';
        
        const newValue = isPlus ? currentValue + 1 : Math.max(1, currentValue - 1);
        input.value = newValue;

        // Sync with main form
        const mainQuantityInput = this.mainForm.querySelector('[name="quantity"]');
        if (mainQuantityInput) {
          mainQuantityInput.value = newValue;
          mainQuantityInput.dispatchEvent(new Event('change', { bubbles: true }));
        }
      });
    });
  }

  onSubmitHandler(evt) {
    evt.preventDefault();
    if (this.submitButton.classList.contains('loading')) return;

    // Enable the variant ID input before submission
    const variantInput = this.stickyForm.querySelector('[name="id"]');
    if (variantInput) {
      variantInput.disabled = false;
    }

    this.handleErrorMessage();
    this.toggleLoadingState(true);

    const formData = new FormData(this.stickyForm);
    const config = {
      method: 'POST',
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'Accept': 'application/javascript'
      },
      body: formData
    };

    fetch(window.Shopify.routes.root + 'cart/add.js', config)
      .then(response => response.json())
      .then((response) => {
        if (response.status) {
          this.handleErrorMessage(response.description);
          return;
        }

        // Update cart count
        this.updateQuantity();

        // Show cart notification if it exists
        if (this.cartNotification) {
          this.cartNotification.renderContents(response);
        }
      })
      .catch((error) => {
        console.error(error);
        this.handleErrorMessage('An error occurred. Please try again.');
      })
      .finally(() => {
        this.toggleLoadingState(false);
      });
  }

  updateQuantity() {
    fetch(window.Shopify.routes.root + 'cart.js')
      .then(response => response.json())
      .then(cart => {
        const cartCountBubble = document.getElementById('cart-icon-bubble');
        if (cartCountBubble) {
          const count = cart.item_count;
          cartCountBubble.textContent = count;
          cartCountBubble.classList.toggle('hidden', count === 0);
        }
      })
      .catch(error => {
        console.error('Error fetching cart:', error);
      });
  }

  toggleLoadingState(isLoading = true) {
    if (this.submitButton) {
      this.submitButton.classList.toggle('loading', isLoading);
      const spinner = this.submitButton.querySelector('.loading-overlay__spinner');
      if (spinner) {
        spinner.classList.toggle('hidden', !isLoading);
      }
    }
  }

  handleErrorMessage(errorMessage = false) {
    if (!this.errorMessageWrapper) return;
    
    this.errorMessageWrapper.toggleAttribute('hidden', !errorMessage);
    if (errorMessage) {
      this.errorMessage.textContent = errorMessage;
    }
  }

  createObserver() {
    let observer = new IntersectionObserver((entries, observer) => {
      this.productFormBounds = entries[0].isIntersecting;
    });
    observer.observe(this.mainProductForm);
  }

  onScroll() {
    this.productFormBounds ? 
      requestAnimationFrame(this.hide.bind(this)) : 
      requestAnimationFrame(this.reveal.bind(this));
  }

  hide() {
    if (this.productStickyForm?.classList.contains('product--sticky-form__active')) {
      this.productStickyForm.classList.remove('product--sticky-form__active');
      this.productStickyForm.classList.add('product--sticky-form__inactive');
    }
  }

  reveal() {
    if (this.productStickyForm?.classList.contains('product--sticky-form__inactive')) {
      this.productStickyForm.classList.add('product--sticky-form__active');
      this.productStickyForm.classList.remove('product--sticky-form__inactive');
    }
  }
});
  1. Make sure your product-sticky-form.liquid has all required elements:
  • Form with correct name attributes
  • Error message wrapper
  • Loading spinner in the submit button
  • Cart notification element

Try this version and let me know if you encounter any specific issues. The code should now properly:

→ Add items to cart
→ Update cart count
→ Show loading states
→ Handle errors
→ Sync between forms
→ Handle variants correctly

Shubham | hello@untechnickle.com