DEV Community

Krypton
Krypton

Posted on

How to implement a scroll snap with intersection observer

I am inspired by Rolls Royce website and want to implement the same scroll snapping feature in mine as well, I did it with the HTML default scroll-snap-type but which gives me expected behavior but creates two scrollbars, one for the container and another one for the body, which is not expected so I tried to go with the IntersectionObserver but it causes an issue, I can travel to only adjacent slide when directly jumping from 1st slide to 3rd slide.

Here is the code sandbox link: https://codesandbox.io/s/scrollsnap-forked-pre0c?file=/pages/index.vue

I am inspired by Rolls Royce website and want to implement the same scroll snapping feature in mine as well, I did it with the HTML default scroll-snap-type but which gives me expected behavior but creates two scrollbars, one for the container and another one for the body, which is…

Here is the code that I am working:

<template>
  <main class="landing">
    <nav class="scroller">
      <ul class="scroller__list">
        <li
          class="scroller__item"
          v-for="(slide, index) in slides"
          :key="index"
          @click.prevent="scroll(slide.id)"
        >
          <a
            class="scroller__dot"
            :href="'#' + slide.id"
            @click.prevent="scroll(slide.id)"
          ></a>
        </li>
      </ul>
    </nav>
    <div class="slides-container">
      <slide
        class="slide"
        v-for="(slide, index) in slides"
        :key="index"
        :img="slide.img"
        :id="slide.id"
        :format="slide.format"
        :filter="slide.filter"
        >{{ slide.content }}</slide
      >
    </div>
  </main>
</template>

<script lang="ts">
import Vue from "vue";

export default Vue.extend({
  data() {
    return {
      slides: [
        {
          img: "car-slide-1.png",
          content: "hello world",
          id: "car-slide-1",
          filter: "color-burn",
        },
        {
          img: "car-slide-2.png",
          content: "Second Car",
          id: "car-slide-2",
          filter: "color-burn",
        },
        {
          img: "car-slide-3.png",
          content: "Third slide",
          id: "car-slide-3",
          filter: "color-burn",
        },
      ],
      observer: null as any as IntersectionObserver,
      options: {
        threshold: [0.5],
        root: process.browser
          ? document.getElementsByClassName("slides-container")[0]
          : null,
      } as IntersectionObserverInit,
    };
  },
  methods: {
    scroll(id: string, who: string | null = null) {
      console.log("scrolling to ", id, who ? "by " + who : "");
      document.getElementById(id)?.scrollIntoView({
        behavior: "smooth",
        block: "start",
      });
    },
  },
  mounted() {
    let scrolling = false;
    this.observer = new IntersectionObserver((entries, observer) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting && !scrolling) {
          let top = entry.boundingClientRect.top;
          scrolling = true;
          window.scroll({ behavior: "smooth", top: window.pageYOffset + top });
        }
        scrolling = false;
      });
    }, this.options);
    document
      .querySelectorAll(".slide")
      .forEach((slide) => this.observer.observe(slide));
  },
});
</script>
Enter fullscreen mode Exit fullscreen mode

Discussion (0)