Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Indicators (dots) with CSS scroll snap

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.

bootstrap screenshot with carousel indicators

Do I need JavaScript for this? (I assume so.) How should I detect which item is currently visible?

like image 339
Dan Fabulich Avatar asked Nov 17 '18 20:11

Dan Fabulich


1 Answers

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>
like image 76
Dan Fabulich Avatar answered Sep 21 '22 11:09

Dan Fabulich