Swatches stop working after filter — need help reinitializing JS + hover image change

Topic summary

A Shopify store owner encountered issues with custom color swatches that change product images on collection pages. The swatches worked on initial page load but broke after applying filters via Shopify’s native Search & Discovery feature.

Core Problem:

  • AJAX-based filter updates removed JavaScript event listeners
  • Hover functionality failed completely (even before filtering)
  • Standard event listeners (DOMContentLoaded, facet:updated) didn’t reliably reinitialize the script

Key Technical Issues Identified:

  • facet:updated event doesn’t exist in Dawn theme
  • Radio inputs were display:none, preventing hover events from firing
  • Event listeners needed to target labels instead of hidden radio buttons
  • Large variantDataMap object (350kb, 10,000+ lines) caused performance issues

Solution Implemented:

  • Event delegation pattern: attached listeners to document instead of individual elements
  • Moved URLs from global data map to individual label data- attributes
  • Added event handlers to labels for click, mouseenter, and mouseleave
  • Used capture:true for mouse events and passive:true for performance
  • Implemented image preloading to eliminate lag

Outcome: Swatches now function correctly after filtering, hover effects work smoothly, and performance improved significantly.

Summarized with AI on October 27. AI used: claude-sonnet-4-5-20250929.

Hi everyone!

I’m working on a Shopify store (Dawn-based) and I’m using a custom JavaScript to make color swatches (variant selectors) change the product image on collection pages. Everything works perfectly on first page load, but breaks when a filter is applied.


:magnifying_glass_tilted_left: The problem:- I’m using Shopify’s native filters (Search & Discovery).

  • After a filter is selected, the product grid is updated via AJAX.

  • The color swatches stop working — they no longer change the image when clicked or hovered.

  • If I reload the page, it works again.


What I’ve tried:- Wrapped my logic in activateColorSwatches()

  • Called it on:

    • DOMContentLoaded

    • facet:updated

    • shopify:section:load, shopify:section:select, etc.

  • Used MutationObserver on .product-grid

→ None of these reliably reapply the behavior after filters.


What I need help with:1. How to reliably reinitialize JS after filters are applied on collection pages

  1. Best way to make hover on color swatches also change the product image (not just on click)

Thanks so much in advance! :folded_hands:
—Ricardo

Hi @ricpina ,

The color swatches stop working after filters because Shopify reloads the product grid using AJAX, which removes your custom JavaScript event listeners.

To fix this, wrap your swatch functionality inside a function (e.g., activateColorSwatches()), and re-run it whenever Shopify updates the grid by listening to the facet:updated event:

document.addEventListener(‘DOMContentLoaded’, activateColorSwatches);
document.addEventListener(‘facet:updated’, activateColorSwatches);

This ensures your swatches work again after every filter change.

For hover functionality, add mouseenter and mouseleave events on the swatches to swap the product image dynamically.

Please let me know if it helps you in any way !

Thanks !

Hi @StevenT_A7 ! Thanks so much for your reply!

I tried implementing your suggestion by wrapping my logic in activateColorSwatches() and calling it on both DOMContentLoaded and facet:updated.

Unfortunately, it still doesn’t work after applying a filter. The swatches only work on the first page load or if I manually refresh the filtered page.

I’m placing this code inside a custom .liquid file called card-product-variant-selection-custom.liquid, which controls the behavior of the product cards on collection pages. So the JavaScript is tied to how the swatches behave in the product grid, not on the product detail page.

I also added mouseenter and mouseleave events to trigger image changes on hover, but hover doesn’t work at all — neither before nor after filtering.

Here’s the full script I’m using:

function activateColorSwatches() {
  console.log('[Vegayn] Activating swatches...');

  document.querySelectorAll('.grid.product-grid').forEach(function(productGrid) {
    var sectionId = productGrid.getAttribute('data-section-id');
    var variantDataMap = window['variantDataMap' + sectionId.replace(/-/g, '_')];
    if (!variantDataMap) return;

    productGrid.querySelectorAll('input[type="radio"][data-section-id="' + sectionId + '"]').forEach(function(radio) {
      const card = radio.closest(`.card-product-custom-div[data-section-id="${sectionId}"]`);
      const variantId = radio.getAttribute('data-variant-id');
      const variantData = variantDataMap[variantId];
      if (!variantData || !card) return;

      const productImageElement = card.querySelector('.card__media img');
      if (!productImageElement) return;

      // Click = change image permanently
      radio.addEventListener('change', function () {
        updateImage(productImageElement, variantData.imageUrl);
        updateLinks(card, variantData.productUrl);
      });

      // Hover = temporary image change
      radio.addEventListener('mouseenter', function () {
        productImageElement.setAttribute('data-original-src', productImageElement.src);
        productImageElement.setAttribute('data-original-srcset', productImageElement.srcset);
        updateImage(productImageElement, variantData.imageUrl);
      });

      radio.addEventListener('mouseleave', function () {
        const originalsrc=productImageElement.getAttribute('data-original-src');
        const originalSrcset = productImageElement.getAttribute('data-original-srcset');
        if (originalSrc && originalSrcset) {
          productImageElement.src=originalSrc;
          productImageElement.srcset = originalSrcset;
        }
      });
    });
  });
}

function updateImage(imgElement, imageUrl) {
  const dynamicSrcset = [
    imageUrl + '?width=165 165w',
    imageUrl + '?width=360 360w',
    imageUrl + '?width=533 533w',
    imageUrl + '?width=720 720w',
    imageUrl + '?width=940 940w',
    imageUrl + '?width=1066 1066w'
  ].join(', ');
  imgElement.srcset = dynamicSrcset;
  imgElement.src=imageUrl;
}

function updateLinks(card, newUrl) {
  card.querySelectorAll('a[id^="CardLink-"], a[id^="StandardCardNoMediaLink-"]').forEach(function(link) {
    link.href = newUrl;
  });
}

document.addEventListener('DOMContentLoaded', activateColorSwatches);
document.addEventListener('facet:updated', activateColorSwatches);

Is there a better event I should be listening for? Or a different approach you’d recommend when working inside a dynamically updated product grid?

Any insights would be super appreciated!

Thank you again!

Hi @ricpina ,

Here is production-ready version of your swatch script to add to your Shopify theme. This goes into your theme.liquid just before the closing tag OR inside your assets/global.js or theme.js if you have one.

Placement Instructions:-

  • If using theme.liquid: place it right before .
  • If using global.js or similar: paste it at the bottom of the file.

Please try this and let me know if this works for you .

Thanks

Ideally you should share a preview link – this would eliminate guesswork.

Briefly:

  • DOMContentLoaded – runs only once on page load

  • facet:updated – Dawn does not generate this event

  • shopify:section:load, shopify:section:select, etc. – fired only in Cusomizer, not on front-end

  • Mutation observer can work, but you need to be careful what you’re observing.

What I’d consider first – if you know that your targets are added-removed dynamically, I’d listen for event on some element which would not change; ultimately you can listen on document. Then simply filter and process the clicks you need. This is called Event delegation.

Kinda like this:

document.documentElement.addEventListener('click', (evt)=> {
  let radio = evt.target.closest('input[type=radio]');
  if (radio) {
    console.log( radio );
    // go do your thing
  }
})

Hi @StevenT_A7 ,

Thanks so much for your detailed reply and for taking the time to put together a production-ready version — I really appreciate it!

Unfortunately, I tried the script exactly as you described (added right before in theme.liquid), but it still didn’t work.

To clarify:

  • The color swatches stop working after filters are applied — exactly as before.

  • The hover effect on color swatches does not work at all, even without any filters.

  • I’m using this inside a custom section file: card-product-variant-selection-custom.liquid.

Maybe there’s something in that custom setup conflicting with the activation?

If it helps, here is a preview link to my store:
:backhand_index_pointing_right: https://1y95fzqgmldlihvt-92359197009.shopifypreview.com/

I’d really appreciate if you could take another look. Let me know if you’d like me to share any specific part of my theme code.

Thanks again!
– Ricardo

Hi @tim_1 , thank you so much for the detailed explanation and suggestion!

I’ve tried implementing the event delegation approach like you outlined.

Unfortunately, I still couldn’t get the swatches to work properly after filters are applied. The console.log never triggers when I click the color swatches inside the filtered product grid.

Just to clarify:

  • I’m using a custom section for the swatches called card-product-variant-selection-custom.liquid.

  • After applying a filter on the collection page, the product grid is updated via AJAX, and the image switching logic (on swatch click or hover) breaks.

  • Even the hover effect doesn’t work at all, even before filtering.

Here’s a preview link in case it helps debug further:
:backhand_index_pointing_right: https://1y95fzqgmldlihvt-92359197009.shopifypreview.com/

I’d love to hear your thoughts or suggestions for a workaround based on this setup.

Thanks again for your time!
– Ricardo

Let’s start with hover – it does not work because it never happens – the radio which you’re listening for events on is “display:none”.

“change” is a different thing (browser triggers change when you click the label), but for “mouseenter” and “mouseleave” you need to attach them to relevant labels.

Actually, you can listen for all three events on label – “click”, “mouseenter” and “mouseleave”.

I would also move the URLs from single variantDataMap onto actual radios/labels. This will simplify the code and also will reduce initial page size a bit – on /collection/t-shirts the size of variantDataMap script is almost 10000 lines and over 350kb in size because it’s data for over 1100 variants.

Say the HTML will be like:


  
  

Then you can easily use delegation:

let eventHandler = (evt) => {
  let label = evt.target.closest('label.color-swatch');
  if (!label) return;

  let productUrl = label.dataset.url;
  let imageUrl = label.dataset.image;
  let card = label.closest('.card-product-custom-div');
  let imageElement = card?.querySelector('img');

  if (productUrl && imageUrl && card && imageElement) {
    switch (evt.type){
      case 'click':
        updateImage(imageElement, imageUrl),
        updateLinks(card, productUrl);
        break;
      case 'mouseenter':
        ...
      case 'mouseleave':
        ...
    }
  }
};
document.documentElement.addEventListener('click', eventHandler, {passive:true});
document.documentElement.addEventListener('mouseenter', eventHandler, {capture:true, passive:true});
document.documentElement.addEventListener('mouseleave', eventHandler, {capture:true, passive:true});

You need capture for mouseover and mouseleave, but not required for click.

1 Like

Hi @tim_1 ,

I just wanted to thank you again for all the support — I’ve implemented the adjusted script with preloading, and it works beautifully now!

The hover effect is smooth, the image updates on click, and there’s no more lag thanks to the preload solution. Your guidance really helped me get this working — especially as I’m still quite new to coding.

Appreciate your time and knowledge!

Best regards,
Ricardo