A before/after slider in Ghost?

Ghost doesn't have native before/after slider support, but it only takes a little bit of JavaScript to make that happen.

A slider bar, showing a before-after comparison of a Ghost feeling happy and grossed out about dog food.
This feature image is non-functional. Try the actual demo below!

CTA Image

As is often the case, if you're reading on email or ActivityPub, you're going to be looking at broken widgets. Come on over to the website to see it in all its glory.

(Aside: Still waiting for a visibility slider that lets me treat ActivityPub like email. I can't turn this notice off on web without it also being off for ActivityPub, where all the JavaScript below will have been stripped out. Argh.)

Read on the web!

Sometimes the Ghost Forum reminds me that I never got around to writing that blog post. This is one of those times.

Here's the code I used previously:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/slider.bundle.js"></script>
<div id="mySlider"></div>

<script>
  new SliderBar({
    el: '#mySlider',         
    beforeImg: 'https://www.spectralwebservices.com/content/images/2025/03/before.png', 
    afterImg: 'https://www.spectralwebservices.com/content/images/2025/03/after.png',    
    width: "100%",             
    height: "505px",            
    line: true,                 // Dividing line, default true
    lineColor: "rgba(0,0,0,0.5)" // Dividing line color, default rgba(0,0,0,0.5)
  });
</script>
<style>
  #mySlider img { max-width: unset;}
</style>

One way to use this code would be to upload the images on the post, right click to copy their locations, and then paste into the code above. Then delete the images from the post.

It makes use of the before-after-slider created by VincentTV and MIT licensed on GitHub. Thanks!

Still... we can probably do better, right? Maybe any time there are two adjacent images, they should go into a slider? Oh, but maybe we have other images that we shouldn't fire on, and they need to be the same size, and I guess we could use captions as a hint, too? Yeah, ok. That'd be good...

But what should I use for a demo? Too often, I see sliders used to show pre- and post-disaster destruction. We could look at satellite images of the house I lived in in graduate school, in Altadena. Hmm. Nah. Too heavy. I must have some images around here somewhere...

A happy ghost with a spoon full of dog food
Before tasting.
An unhappy and grossed out ghost with a spoon full of dog food
After tasting. Who said dogfooding was a good idea for software engineers, anyway!?

Here are some other images that don't get slider-ized:

And here's a second slider, to show that two works!

Before I tried lifting the huge box of books
After I filled the huge box with books

Here's the code. You could put it into code injection sitewide if you wanted to, or make it an html card snippet for occasional user.

Note that this code only fires if you have two images with identical sizes (because things don't work well otherwise) that are adjacent, and with one caption that starts with "before" and the other that starts with "after". So it should be pretty safe to run sitewide.

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/slider.bundle.js"></script>

<script>
  // Function to detect and initialize sliders with adjacent images
  function initializeSliderFromAdjacentImages() {
    // Find all image cards
    const imageCards = document.querySelectorAll('figure.kg-card.kg-image-card.kg-card-hascaption');
    
    if (imageCards.length < 2) {
      return;
    }
    
    let sliderCounter = 0;
    const processedIndices = new Set();
    
    // Check for adjacent pairs with matching dimensions
    for (let i = 0; i < imageCards.length - 1; i++) {
      // Skip if either card has already been processed
      if (processedIndices.has(i) || processedIndices.has(i + 1)) {
        continue;
      }
      
      const firstCard = imageCards[i];
      const secondCard = imageCards[i + 1];
      
      // Get the img elements
      const firstImg = firstCard.querySelector('img.kg-image');
      const secondImg = secondCard.querySelector('img.kg-image');
      
      if (!firstImg || !secondImg) {
        continue;
      }
      
      // Get caption text and check for "Before" and "After"
      const firstCaption = firstCard.querySelector('figcaption');
      const secondCaption = secondCard.querySelector('figcaption');
      if (!firstCaption || !secondCaption) {
        continue;
      }
      const firstCaptionText = firstCaption.textContent.trim().toLowerCase();
      const secondCaptionText = secondCaption.textContent.trim().toLowerCase();
      const firstIsBefore = firstCaptionText.startsWith('before');
      const firstIsAfter = firstCaptionText.startsWith('after');
      const secondIsBefore = secondCaptionText.startsWith('before');
      const secondIsAfter = secondCaptionText.startsWith('after');
      if (!((firstIsBefore && secondIsAfter) || (firstIsAfter && secondIsBefore))) {
        continue;
      }
      // If you don't want caption checking, delete everything from "Get the caption text..." to here.
      
      // Check if they have the same width and height
      const firstWidth = firstImg.getAttribute('width');
      const firstHeight = firstImg.getAttribute('height');
      const secondWidth = secondImg.getAttribute('width');
      const secondHeight = secondImg.getAttribute('height');
      
      if (firstWidth === secondWidth && firstHeight === secondHeight && firstWidth && firstHeight) {
        // Get the src locations
        const firstSrc = firstImg.getAttribute('src');
        const secondSrc = secondImg.getAttribute('src');
        
        if (firstSrc && secondSrc) {
          // Determine which image is "before" and which is "after"
          let beforeSrc, afterSrc;
          if (firstIsBefore) {
            beforeSrc = firstSrc;
            afterSrc = secondSrc;
          } else {
            beforeSrc = secondSrc;
            afterSrc = firstSrc;
          }

          // Create unique ID for this slider
          const sliderId = 'imageSlider' + sliderCounter;
          sliderCounter++;
          
          // Create slider container div
          const sliderContainer = document.createElement('div');
            sliderContainer.id = sliderId;          firstCard.parentNode.insertBefore(sliderContainer, firstCard);
          
          firstCard.remove();
          secondCard.remove();
          
          processedIndices.add(i);
          processedIndices.add(i + 1);
          
          // Initialize the sliderbar
          new SliderBar({
            el: '#' + sliderId,
            beforeImg: beforeSrc,
            afterImg: afterSrc,
            width: "90%",
            height: "auto",
            line: true,
            lineColor: "rgba(0,0,0,0.5)"
          });
        }
      }
    }
    
    if (sliderCounter === 0) {
      console.log('No matching adjacent images found');
    } else {
      console.log('Initialized ' + sliderCounter + ' slider(s)');
    }
  }
  
  // Initialize when DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initializeSliderFromAdjacentImages);
  } else {
    initializeSliderFromAdjacentImages();
  }
</script>
<style>
  /* My theme needed this. YMMV. If your images are squishing instead of sliding, you may need something similar. */
  [id^="imageSlider"] img { max-width: min(100vw, var(--content-width,720px));}
</style>

But what about emails? (And ActivityPub)

Email clients don't run JavaScript. Instead, you'll get two adjacent images, with the captions you set. So... not broken, but not a functional slider, either.


That's all for today!

I'd love to see how you use this slider! Drop a link in the comments, ok? Happy comparison-making!


Hey, before you go... If your finances allow you to keep this tea-drinking ghost and the freelancer behind her supplied with our hot beverage of choice, we'd both appreciate it!