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.
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.)
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...


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


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


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!