Slideshow
Very simple solution for creating a slideshow which can contain any content you desire and not just limited to images.
Supports the ability to be toggled on/off depending on viewport width. This would for example allow you to have a grid of 8 cards on viewports 1280px wide or larger and then stack the cards into a slideshow where each card is overlaying one another on viewports smaller than 1280px.
Supports the ability for previous / next buttons and pagination dots.
Pagination dots can be placed anywhere in the page DOM and matched to the specific gallery via its data-slideshow-id.
This was originally written on 2023-10-04 but has been updated as needed.
HTML
<div class="codepen-demo-wrapper">
<!-- --------------------------
slideshow: start
--------------------------- -->
<div class="slideshow__container" data-slideshow-interval="5000" data-slideshow-id="1">
<!-- slide 1 -->
<div class="slideshow__slide">
<img src="https://alxwntr.com/codepen-assets/slideshow/photos/01.jpg" width="250" height="250" alt="Slide 1" loading="lazy" draggable="false">
</div>
<!-- slide 2 -->
<div class="slideshow__slide">
<img src="https://alxwntr.com/codepen-assets/slideshow/photos/02.jpg" width="250" height="250" alt="Slide 2" loading="lazy" draggable="false">
</div>
<!-- slide 3 -->
<div class="slideshow__slide">
<img src="https://alxwntr.com/codepen-assets/slideshow/photos/03.jpg" width="250" height="250" alt="Slide 3" loading="lazy" draggable="false">
</div>
<!-- slide 4 -->
<div class="slideshow__slide">
<img src="https://alxwntr.com/codepen-assets/slideshow/photos/04.jpg" width="250" height="250" alt="Slide 4" loading="lazy" draggable="false">
</div>
<!-- slide 5 -->
<div class="slideshow__slide">
<img src="https://alxwntr.com/codepen-assets/slideshow/photos/05.jpg" width="250" height="250" alt="Slide 5" loading="lazy" draggable="false">
</div>
<!-- controls -->
<div class="slideshow__controls slideshow__controls--overlay">
<button class="slideshow__prev" aria-label="Previous Slide">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM199 303L279 223C288.4 213.6 303.6 213.6 312.9 223C322.2 232.4 322.3 247.6 312.9 256.9L273.9 295.9L424 295.9C437.3 295.9 448 306.6 448 319.9C448 333.2 437.3 343.9 424 343.9L273.9 343.9L312.9 382.9C322.3 392.3 322.3 407.5 312.9 416.8C303.5 426.1 288.3 426.2 279 416.8L199 336.8C189.6 327.4 189.6 312.2 199 302.9z"/></svg>
</button>
<button class="slideshow__next" aria-label="Next Slide">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM199 303L279 223C288.4 213.6 303.6 213.6 312.9 223C322.2 232.4 322.3 247.6 312.9 256.9L273.9 295.9L424 295.9C437.3 295.9 448 306.6 448 319.9C448 333.2 437.3 343.9 424 343.9L273.9 343.9L312.9 382.9C322.3 392.3 322.3 407.5 312.9 416.8C303.5 426.1 288.3 426.2 279 416.8L199 336.8C189.6 327.4 189.6 312.2 199 302.9z"/></svg>
</button>
</div>
</div>
<!-- dots -->
<div class="slideshow__dots slideshow__dots--style-default" data-slideshow-id="1"></div>
<!-- --------------------------
slideshow: end
--------------------------- -->
</div>
CSS
// ***********************************
// Slideshow - Container + Slides
// ***********************************
.slideshow__container {
&:not(.kill-slideshow) {
display: grid;
place-items: start;
.slideshow__slide {
opacity: 0;
z-index: -1;
grid-column: 1 / 2;
grid-row: 1 / 2;
transition: all 1s;
}
.slideshow__slide[data-slideshow-status='active'] {
opacity: 1;
z-index: 1;
}
}
}
// ***********************************
// Slideshow - Dots
// ***********************************
.slideshow__dots {
display: flex;
.slideshow__dot {
cursor: pointer;
}
}
// ***********************************
// Slideshow - Dots - Style 1
// ***********************************
.slideshow__dots--style-default {
gap: 0.25em;
justify-content: center;
.slideshow__dot {
background-color: #000;
border: none;
border-radius: 100%;
height: 1rem;
width: 1rem;
}
.slideshow__dot[data-slideshow-status='active'] {
background-color: crimson;
}
}
// ***********************************
// Slideshow - Controls
// ***********************************
.slideshow__controls {
display: flex;
justify-content: space-between;
}
// ***********************************
// Slideshow - Controls - Overlay
// Optional
// ***********************************
.slideshow__controls--overlay {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
z-index: 1000;
}
// ***********************************
// Slideshow - Previous / Next
// ***********************************
.slideshow__prev,
.slideshow__next {
align-items: center;
background-color: transparent;
border: 0;
cursor: pointer;
display: flex;
flex-direction: column;
height: 50px;
justify-content: center;
transition: all 0.5s;
width: 50px;
&:hover,
&:focus {
svg {
path {
fill: crimson;
}
}
}
}
.slideshow__prev {
margin-left: 1rem;
}
.slideshow__next {
margin-right: 1rem;
svg {
transform: rotate(180deg);
}
}
// ***********************************
// CodePen Demo
// Not intended for use outside of this demo.
// ***********************************
body {
background-color: ghostwhite;
display: grid;
height: 100vh;
margin: 0;
place-items: center;
}
.codepen-demo-wrapper {
grid-column-end: 2;
grid-column-start: 1;
grid-row-end: 2;
grid-row-start: 1;
width: fit-content;
}
.slideshow__container {
background: #e0f2f1;
padding: 1rem;
position: relative;
width: 250px;
img {
display: block;
}
}
.slideshow__dots {
background: #d6e5e4;
padding: 1rem;
width: 250px;
}
JavaScript
// ***********************************
// Slideshow - Fade In / Out
// For simple slideshows.
// by Alex Winter - 2023-10-04
// v1.0.3
// ***********************************
// example usage:
// <div class="slideshow__container" data-slideshow-viewport-width-activate="960" data-slideshow-interval="5000" data-slideshow-id="1">
// <div class="slideshow__slide" data-slideshow-status="active"> ... </div>
// <div class="slideshow__slide"> ... </div>
// <!-- (etc... as many slides as needed) -->
// <!-- optional: -->
// <div class="slideshow__dots slideshow__dots--style-default" data-slideshow-id="1"></div>
// <div class="slideshow__controls">
// <button class="slideshow__prev" aria-label="Previous Slide"></button>
// <button class="slideshow__next" aria-label="Next Slide"></button>
// </div>
// </div>
// Version Log:
//
// v1.0.2 - 2025-06-16
// - added 'slideshow__dots--style-default' and 'slideshow__dots--style-alt-1' classes for different dot styles
//
// v1.0.2 - 2025-06-16
// - added 'checkViewportWidth' which looks for the `data-slideshow-viewport-width-activate` data attribute in the `slideshow__container` element. The value should be numeric to represent the width of the viewport you wish to have the slideshow activate at. For example, if set to '960' then the slideshow will be deactivated on viewports greater than 960 and activated on viewports less than 960.
//
// v1.0.1 - 2025-03-27
// - `slideshow__dots` element can now be placed outside of the `slideshow__container` element. You must set a `data-slideshow-id=""` attribute within the `slideshow__container` tag and the `slideshow__dots` tag so that the script can associate the two elements together. The value of the data attributes should be numeric such as '1', '2', '3', etc per each respective slideshow so that the script can match them.
// - Renamed 'data-interval' attribute to 'data-slideshow-interval'
// - Renamed 'data-status' attribute to 'data-slideshow-status'
//
// v1.0.0 - 2023-10-04
// Initial version.
document.addEventListener('DOMContentLoaded', () => {
const slideshows = document.querySelectorAll('.slideshow__container');
slideshows.forEach((slideshow) => {
// console.log('Slideshow container found'); // debug
const slideshowId = slideshow.getAttribute('data-slideshow-id');
const slides = slideshow.querySelectorAll('.slideshow__slide');
const slideshowControls = slideshow.querySelectorAll('.slideshow__controls');
const slideshowDots = document.querySelectorAll('.slideshow__dots');;
const viewportWidthActivate = slideshow.getAttribute('data-slideshow-viewport-width-activate');
const interval = slideshow.getAttribute('data-slideshow-interval') || 5000; // Default to 5000 if not set
const slideCount = slides.length; // // total amount of slides
let currentSlideIndex = 0; // initial index
let slideTimeout; // store the timeout ID and clear it when needed
// recursive function that allows us to loop through the slides
// initially we set the slideIndex to 0 to display the first slide
// then we add 1 to the current slideIndex and use the modulo operator to get the remainder
// this determines the next slide to display and loops back to the first slide when the last slide is reached
const showSlide = (slideIndex) => {
// console.log('Showing slide:', slideIndex); // debug
if (slideIndex < 0) return; // return if there are no slides
if (slideshow.classList.contains('kill-slideshow')) return; // if this class exists, don't run the slideshow. This is used for determining whether or not the slideshow should or should not run in various viewport sizes
// remove status from all slides
slides.forEach((slide) => {
slide.removeAttribute('data-slideshow-status');
});
slides[slideIndex].setAttribute('data-slideshow-status', 'active'); // set status to active on the current slide
// set dot status
if (slideshowDots) {
slideshowDots.forEach((dots) => {
const dotsId = dots.getAttribute('data-slideshow-id');
if (dotsId == slideshowId) {
const slideshowDotsButtons = dots.querySelectorAll('.slideshow__dot');
slideshowDotsButtons.forEach((button) => {
button.removeAttribute('data-slideshow-status');
});
slideshowDotsButtons[slideIndex].setAttribute('data-slideshow-status', 'active');
}
})
}
// go to the next slide and loop back to the first slide when the last slide is reached
clearTimeout(slideTimeout);
slideTimeout = setTimeout(() => {
let newSlideIndex = (slideIndex + 1) % slideCount;
showSlide(newSlideIndex);
}, interval);
currentSlideIndex = slideIndex;
}
// ***********************************
// Controls - Prev / Next
// ***********************************
const showControls = () => {
if (!slideshowControls) return;
slideshowControls.forEach((controls) => {
const controlsPrev = controls.querySelector('.slideshow__prev');
const controlsNext = controls.querySelector('.slideshow__next');
controlsPrev.addEventListener('click', () => {
// if the current slide is the first slide, then we want to go to the very last slide
if (currentSlideIndex === 0) {
currentSlideIndex = slideCount;
}
// otherwise we want to go to the previous slide
let newSlideIndex = (currentSlideIndex - 1) % slideCount;
showSlide(newSlideIndex);
setDotStatus(); // set dot status
});
controlsNext.addEventListener('click', () => {
// go to the next slide
// but if the current slide is the last slide, then we want to go to the very first slide
let newSlideIndex = (currentSlideIndex + 1) % slideCount;
showSlide(newSlideIndex);
setDotStatus(); // set dot status
});
});
}
showControls();
// ***********************************
// Dots - Jump to Slide Buttons
// ***********************************
const showDots = () => {
const slideshowDots = document.querySelectorAll('.slideshow__dots');
if (!slideshowDots) return;
slideshowDots.forEach((dots) => {
const dotsId = dots.getAttribute('data-slideshow-id');
if (dotsId == slideshowId) {
// create dots for each slide
slides.forEach((slide, index) => {
const dot = document.createElement('button');
dot.classList.add('slideshow__dot');
dot.setAttribute('data-slide', index);
dot.setAttribute('aria-label', 'Go to slide ' + index);
dots.appendChild(dot);
// initially set the first dot to active
if (index === 0) {
dot.setAttribute('data-slideshow-status', 'active');
}
});
const slideshowDotButtons = dots.querySelectorAll('.slideshow__dot');
slideshowDotButtons.forEach((dot) => {
dot.addEventListener('click', () => {
showSlide(parseInt(dot.getAttribute('data-slide')));
slideshowDotButtons.forEach((allDots) => {
allDots.removeAttribute('data-slideshow-status');
});
dot.setAttribute('data-slideshow-status', 'active');
});
});
} else {
return;
}
});
}
showDots();
// for setting dot status when prev/next buttons are clicked and during the setInterval
const setDotStatus = () => {
const slideshowDots = document.querySelectorAll('.slideshow__dots');
if (!slideshowDots) return;
slideshowDots.forEach((dots) => {
const dotsId = dots.getAttribute('data-slideshow-id');
if (dotsId == slideshowId) {
const slideshowDotButtons = dots.querySelectorAll('.slideshow__dot');
slideshowDotButtons.forEach((dot) => {
dot.removeAttribute('data-slideshow-status');
if (currentSlideIndex === parseInt(dot.getAttribute('data-slide'))) {
dot.setAttribute('data-slideshow-status', 'active');
}
});
} else {
return;
}
});
}
// ***********************************
// Start the Slideshow
// ***********************************
showSlide(currentSlideIndex);
// ***********************************
// Configure Responsive Slideshows
// ***********************************
const checkViewportWidth = () => {
if (viewportWidthActivate) {
if (window.innerWidth >= viewportWidthActivate) {
slideshow.classList.add('kill-slideshow');
} else if (window.innerWidth < viewportWidthActivate) {
slideshow.classList.remove('kill-slideshow');
showSlide(currentSlideIndex);
}
}
}
checkViewportWidth();
window.addEventListener('resize', checkViewportWidth);
});
});