Auto Image Cycle (Slideshow)
A pure-CSS-transition slideshow that automatically cycles through images at a configurable interval. The script manages three states — active, previous, and not-active — as data attributes, enabling a smooth cross-fade. Uses IntersectionObserver to pause cycling when the component is off-screen.
Code
<div class="image-cycle-collection">
<div class="image-cycle-collection__before"></div>
<div class="image-cycle-collection__list" data-image-cycle="2.5">
<div class="image-cycle-collection__img" data-image-cycle-item="" class="image-cycle-collection__item">
<img src="https://cdn.prod.website-files.com/679e2a340ce9c67cecbca3ad/679e2c81170b6a90046718a2_image-1.webp" loading="lazy" alt="">
</div>
<div class="image-cycle-collection__img" data-image-cycle-item="" class="image-cycle-collection__item">
<img src="https://cdn.prod.website-files.com/679e2a340ce9c67cecbca3ad/679e2c80ab2f91466875df0b_image-2.webp" loading="lazy" alt="">
</div>
<div class="image-cycle-collection__img" data-image-cycle-item="" class="image-cycle-collection__item">
<img src="https://cdn.prod.website-files.com/679e2a340ce9c67cecbca3ad/679e2c80f4b142f1a685a2ee_image-3.webp" loading="lazy" alt="">
</div>
<div class="image-cycle-collection__img" data-image-cycle-item="" class="image-cycle-collection__item">
<img src="https://cdn.prod.website-files.com/679e2a340ce9c67cecbca3ad/679e2c801b8a79e8393b622b_image-4.webp" loading="lazy" alt="">
</div>
<div class="image-cycle-collection__img" data-image-cycle-item="" class="image-cycle-collection__item">
<img src="https://cdn.prod.website-files.com/679e2a340ce9c67cecbca3ad/679e2c803f7f3ff9ad22d21b_image-5.webp" loading="lazy" alt="">
</div>
</div>
</div>.image-cycle-collection {
width: min(95vw, 60em);
position: relative;
}
.image-cycle-collection__before {
padding-top: 66.666%;
}
.image-cycle-collection__list {
z-index: 0;
border-radius: 2em;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
}
.image-cycle-collection__item {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
[data-image-cycle-item="active"] {
transition: opacity 0.4s ease 0s, visibility 0s ease 0s;
opacity: 1;
visibility: visible;
z-index: 3;
}
[data-image-cycle-item="previous"] {
transition: opacity 0.4s ease 0.4s, visibility 0s ease 0.4s;
opacity: 0;
visibility: visible;
z-index: 2;
}
[data-image-cycle-item="not-active"] {
opacity: 0;
visibility: hidden;
z-index: 1;
}
.image-cycle-collection__img {
object-fit: cover;
width: 100%;
height: 100%;
position: absolute;
}
@media screen and (max-width: 767px) {
.image-cycle-collection__list {
border-radius: 1em;
}
}function initImageCycle() {
document.querySelectorAll("[data-image-cycle]").forEach(cycleElement => {
const items = cycleElement.querySelectorAll("[data-image-cycle-item]");
if (items.length < 2) return;
let currentIndex = 0;
let intervalId;
// Get optional custom duration (in seconds), fallback to 2000ms
const attrValue = cycleElement.getAttribute("data-image-cycle");
const duration = attrValue && !isNaN(attrValue) ? parseFloat(attrValue) * 1000 : 2000;
const isTwoItems = items.length === 2;
// Initial state
items.forEach((item, i) => {
item.setAttribute("data-image-cycle-item", i === 0 ? "active" : "not-active");
});
function cycleImages() {
const prevIndex = currentIndex;
currentIndex = (currentIndex + 1) % items.length;
items[prevIndex].setAttribute("data-image-cycle-item", "previous");
if (!isTwoItems) {
setTimeout(() => {
items[prevIndex].setAttribute("data-image-cycle-item", "not-active");
}, duration);
}
items[currentIndex].setAttribute("data-image-cycle-item", "active");
}
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && !intervalId) {
intervalId = setInterval(cycleImages, duration);
} else {
clearInterval(intervalId);
intervalId = null;
}
}, { threshold: 0 });
observer.observe(cycleElement);
});
}
// Initialize Image Cycle
document.addEventListener('DOMContentLoaded', function() {
initImageCycle();
});Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| data-image-cycle | attribute (number, optional) | — | Add to the list container to mark it as a cycle group. The optional value sets the interval in seconds. Omit the value or leave it empty to default to 2000 ms. |
| data-image-cycle-item | attribute (string, managed by JS) | — | Added to each slide element. The script sets its value to "active", "previous", or "not-active" on every cycle tick. Use these attribute selectors in CSS to drive the transition. |
Notes
- •No dependencies — no GSAP or external libraries required.
- •The IntersectionObserver pauses the interval when the component is scrolled out of view and restarts it when it re-enters, saving CPU on long pages.
- •The "previous" state keeps the outgoing slide visible during the opacity transition, ensuring a smooth cross-fade rather than a hard cut.
- •For exactly 2 images, the previous state is never cleared to not-active — this avoids a flash caused by the outgoing image disappearing too early.
- •Multiple [data-image-cycle] groups on the same page are fully independent — each has its own interval and observer.
Guide
Controlling the cycle speed
Pass a number (in seconds) directly on the [data-image-cycle] attribute. Each group can have its own speed independently.
<!-- Defaults to 2000ms -->
<div data-image-cycle>...</div>
<!-- Custom interval: 3 seconds -->
<div data-image-cycle="3">...</div>Slide states
Each slide item receives one of three attribute values that you can target in CSS to animate transitions: "active" (currently visible), "previous" (fading out, kept visible briefly to allow the fade transition to complete), and "not-active" (hidden, no transition).
Aspect ratio
The .image-cycle-collection__before div uses a padding-top percentage to maintain a fixed aspect ratio without a fixed height — 66.666% produces a 3:2 ratio. Change this value to adjust the ratio without touching the absolute-positioned list.
Multiple groups on one page
Each [data-image-cycle] element gets its own interval and IntersectionObserver instance. Add as many groups as needed — they all run independently.