Basic
Thumbnails
Add interactive thumbnail navigation with IntersectionObserver.
const slides = document.querySelectorAll(".slide");
const thumbnails = document.querySelectorAll(".thumbnail");
let activeIndex = 0;
const intersectionRatioBySlide = new WeakMap<Element, number>();
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
intersectionRatioBySlide.set(
entry.target,
entry.isIntersecting ? entry.intersectionRatio : 0,
);
}
const mostVisible = Array.from(slides).reduce(
(current, slide, index) => {
const ratio = intersectionRatioBySlide.get(slide) ?? 0;
return ratio > current.ratio ? { index, ratio } : current;
},
{ index: activeIndex, ratio: 0 },
);
if (mostVisible.ratio > 0) {
activeIndex = mostVisible.index;
thumbnails.forEach((thumb, i) =>
thumb.classList.toggle("active", i === activeIndex),
);
}
},
{ threshold: [0, 0.5, 1] },
);
slides.forEach((slide) => observer.observe(slide));
thumbnails.forEach((thumbnail, index) => {
thumbnail.addEventListener("click", () => {
slides[index]?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
});
});
.carousel {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 100%;
scroll-snap-type: x mandatory;
aspect-ratio: 1;
max-width: 20rem;
}
.slide {
width: 100%;
scroll-snap-align: center;
}
.thumbnails {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.thumbnail {
opacity: 0.4;
}
.thumbnail.active {
opacity: 1;
}
<BlossomCarousel class="carousel">
<div class="slide"><img src="..." alt="" /></div>
<div class="slide"><img src="..." alt="" /></div>
<div class="slide"><img src="..." alt="" /></div>
...
</BlossomCarousel>
<div class="thumbnails">
<button class="thumbnail"><img src="..." alt="" /></button>
<button class="thumbnail"><img src="..." alt="" /></button>
<button class="thumbnail"><img src="..." alt="" /></button>
...
</div>