Home/Journal/Debugging Slick Slider: The Ghost Slide Mystery

January 21, 2025

Debugging Slick Slider: The Ghost Slide Mystery

A deep dive into debugging Slick Slider when ghost slides persist on real mobile devices. Learn how to fully remove unwanted images and ensure Slick updates properly.

Here's a fun one. I was removing lifestyle images from a product page slider (Slick Slider), and on desktop it worked great. On mobile emulators, also great. On an actual physical device, the images disappeared but the slides stayed, just sitting there empty.

Ghost slides.

What was supposed to happen

The goal was pretty simple: find images matching a lifestyle pattern (like LS-1234.jpg), remove them from the carousel, and reinitialize Slick so the layout adjusted. A few lines of code.

The code was doing four things:

  1. Finding images matching the pattern
  2. Removing them from the DOM
  3. Calling slickRemove on the matching slides
  4. Destroying and reinitializing Slick

On desktop and emulators it worked. On a real phone, I got hollow slides with nothing in them.

What didn't work

I tried a few things before landing on the fix.

Just calling .remove() on the <img> elements: the images disappeared, but Slick's internal state still thought those slides existed.

Using .slickRemove(index): worked sometimes on mobile, not reliably.

Calling .slick('setPosition'): did nothing about the extra slides.

The confusing part was that the DOM inspector looked fine. Slick just maintains its own internal slide count separately, and it wasn't letting go of the old state.

What actually worked

The fix was a three-step cleanup.

1. Remove images and their wrappers

Instead of targeting just the <img>, I went up to the parent .slick-slide and removed the whole wrapper:

const removeLifestyleImages = async (container) => {
  if (!container) return;
 
  const regex = /(LS-\d+|LS\d+|alt\d+|alt-\d+|lifestyle\d+)$/i;
  container.querySelectorAll('img').forEach((img) => {
    if (regex.test(img.src)) {
      let wrapper = img.closest('.slick-slide');
      if (wrapper) {
        wrapper.remove();
      }
    }
  });
};

2. Clean up any remaining empty slides

Even after that, sometimes an orphaned .slick-slide would hang around with no content. Added a pass to catch those:

const manuallyRemoveExtraSlides = () => {
  document.querySelectorAll('.slick-slide').forEach((slide) => {
    if (!slide.innerHTML.trim()) {
      slide.remove();
    }
  });
};

3. Destroy and reinitialize with a delay

Slick needs a moment. If you reinitialize immediately after modifying the DOM, it doesn't always catch up. A 300ms delay before reinitializing fixed the remaining weirdness:

const resetSlick = () => {
  const slickContainer = $('.product-image');
 
  if (slickContainer.hasClass('slick-initialized')) {
    slickContainer.slick('unslick');
  }
 
  setTimeout(() => {
    slickContainer.slick({
      infinite: true,
      slidesToShow: 1,
      slidesToScroll: 1,
      dots: true,
      arrows: false,
    });
  }, 300);
};

Putting it all together

const hideLifestyleImages = async () => {
  const productMedia = document.querySelector('.product.media');
  await removeLifestyleImages(productMedia);
  manuallyRemoveExtraSlides();
  resetSlick();
};
 
hideLifestyleImages();

What I took away from this

Always test on a real device. Emulators are close, but mobile Chrome behaves differently enough from desktop that you can have a feature that looks done and isn't. Literal edge case, lol.

Also: Slick tracks its own state separately from the DOM. If you modify slides manually, you kind of have to brute-force it into updating by destroying and rebuilding the whole thing. A little annoying, but reliable once you commit to it.

No more ghost slides.