I'm building a scrolling carousel using CSS scroll snap.
.carousel {
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
width: 100vw;
white-space: nowrap;
overflow-x: scroll;
}
.carousel > * {
display: inline-block;
scroll-snap-align: start;
width: 100vw;
height: 100vh;
}
#x {
background-color: pink;
}
#y {
background-color: lightcyan;
}
#z {
background-color: lightgray;
}
<div class="carousel">
<div id="x">x</div>
<div id="y">y</div>
<div id="z">z</div>
</div>
It works pretty well; if you scroll the carousel with the trackpad or with a finger, it snaps to each item.
What I'd like to add to this are "indicators." e.g. in Bootstrap, these are little dots or lines indicating that there are multiple things in the carousel, one indicator per item in the carousel, with the current item highlighted. Clicking on an indicator will typically scroll you to the specified item.
Do I need JavaScript for this? (I assume so.) How should I detect which item is currently visible?
The way to handle this in modern browsers is with IntersectionObserver
. In the below example, the IntersectionObserver
fires its callback whenever one of the child elements crosses the 50% threshold, making it the element with the largest intersectionRatio
. When this happens, we re-render the indicator based on the index of the currently selected item.
Note that Safari was a bit late in implementing support for IntersectionObserver
. IntersectionObserver just became available in Safari 12.1. You can use the IntersectionObserver polyfill to support older browsers, using this script tag:
<script src="https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script>
Bonus tip: You can use scroll-behavior: smooth
to animate the scrollIntoView
call.
var carousel = document.querySelector('.carousel');
var indicator = document.querySelector('#indicator');
var elements = document.querySelectorAll('.carousel > *');
var currentIndex = 0;
function renderIndicator() {
// this is just an example indicator; you can probably do better
indicator.innerHTML = '';
for (var i = 0; i < elements.length; i++) {
var button = document.createElement('button');
button.innerHTML = (i === currentIndex ? '\u2022' : '\u25e6');
(function(i) {
button.onclick = function() {
elements[i].scrollIntoView();
}
})(i);
indicator.appendChild(button);
}
}
var observer = new IntersectionObserver(function(entries, observer) {
// find the entry with the largest intersection ratio
var activated = entries.reduce(function (max, entry) {
return (entry.intersectionRatio > max.intersectionRatio) ? entry : max;
});
if (activated.intersectionRatio > 0) {
currentIndex = elementIndices[activated.target.getAttribute("id")];
renderIndicator();
}
}, {
root:carousel, threshold:0.5
});
var elementIndices = {};
for (var i = 0; i < elements.length; i++) {
elementIndices[elements[i].getAttribute("id")] = i;
observer.observe(elements[i]);
}
.carousel {
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
width: 100vw;
white-space: nowrap;
overflow-x: scroll;
scroll-behavior: smooth
}
.carousel > * {
display: inline-block;
scroll-snap-align: start;
width: 100vw;
height: 80vh;
}
#x {
background-color: pink;
}
#y {
background-color: lightcyan;
}
#z {
background-color: lightgray;
}
<script src="https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script>
<div class="carousel">
<div id="x">x</div>
<div id="y">y</div>
<div id="z">z</div>
</div>
<div id="indicator">• ◦ ◦</div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With