CiptoHartanto

Vertical Carousel (Snap-Scroll Effect) With Swiper.js

Published on Apr 19, 2024
  • React.js
  • Swiper.js

Background

Before we go ahead, I'd like to present the V.L.O Promotion website to you.

The V.L.O Promotion project was inspired by this website, https://www.baemin.com/. I personally have been working with Swiper.js for sometime and feel this effect could be done through this plug-in.

Challenges

  • The first 4 sections are bound in a snap-scroll effect.
  • From the 5th section onwards, everything should scroll normally.

Investigating Swiper.js

The first 4 sections seem to be a typical Carousel. The difference between vertical and horizontal carousels, besides the opposite orientations, is mostly how we interact with them. In this case, the user moves to the previous or next slide through scrolling.

In this example by Swiper.js team, Swiper.js seems to be capable of creating the vertical carousel just like in our design. However, the only interaction mode it offers is either through clicking the navigation bullets or swiping (grabbing and pushing the slide up or down) the carousel. It's certainly cool, but let's look at the API documentation so we can use scrolling to navigate through the slides.

Under Mousewheel Control Parameters, there are some parameters that could help us get the scroll event going and update the slides in the carousel. Let's keep that in mind for now while we figure out how to make the 5th slide and so forth to be free from the snap-scroll territory.

So, for the 5th slide going forward, we want to make it free scroll which means no snap-scroll effect applied to these slides. Freemode could be the rescue to this behavior. It looks pretty promising as in this demo.

Also, since there's a <Swiper /> that is nested inside a parent <Swiper />, we need to include nested property to the nested one. As stated in the document, 'Set to true on Swiper for correct touch events interception. Use only on swipers that use same direction as the parent one'.

Markup structure

Ideally, slide number 1 through 4 will be wrapped in a <Swiper /> component. The rest would be in the nested <Swiper />.

1// rough markup
2export default function CustomSnapScroll () {
3    return (
4       <Swiper> /* snap-scroll wrapper */ 
5          <SwiperItem>Slide 1</SwiperItem> 
6          <SwiperItem>Slide 2</SwiperItem> 
7          <SwiperItem>Slide 3</SwiperItem> 
8          <SwiperItem>Slide 4</SwiperItem>
9          <SwiperItem> /* non snap-scroll wrapper */
10            <Swiper> /* nested Swiper for slides that should scroll normally */
11                <SwiperItem>Slide 5</SwiperItem>
12                <SwiperItem>Slide 6</SwiperItem>
13                ...
14            </Swiper>
15          </SwiperItem> 
16       </Swiper>
17    )
18}
19
20

Now, let's put in the necessary props and components from Swiper.js. Hopefully, in this simple markup structure, we could see to utilize <Swiper /> and nest a child <Swiper /> into it.

1import { FreeMode, Mousewheel } from "swiper/modules";
2import { Swiper, SwiperSlide } from "swiper/react";
3
4// Import Swiper styles
5import "swiper/css";
6
7export default function CustomSnapScroll () {
8    return (
9       <Swiper
10        slidesPerView={1}
11        modules={[Mousewheel]}
12        direction="vertical"
13        mousewheel={true}
14       >
15          <SwiperSlide>Slide 1</SwiperSlide> 
16          <SwiperSlide>Slide 2</SwiperSlide> 
17          <SwiperSlide>Slide 3</SwiperSlide> 
18          <SwiperSlide>Slide 4</SwiperSlide>
19          <SwiperSlide>
20            <Swiper
21              modules={[FreeMode, Mousewheel]}
22              freeMode={{ sticky: false, momentumRatio: 0.5 }}
23              direction="vertical"
24              mousewheel={true}
25              nested
26            >
27                <SwiperSlide>Slide 5</SwiperSlide>
28                <SwiperSlide>Slide 6</SwiperSlide>
29                ...
30            </Swiper>
31          </SwiperSlide> 
32       </Swiper>
33    )
34}
35
36

End Results

Here's the final code with styling. The custom Snap-scroll is now ready! You can also check the the working sample here:

1import { FreeMode, Mousewheel } from "swiper/modules";
2import { Swiper, SwiperSlide } from "swiper/react";
3
4// Import Swiper styles
5import "swiper/css";
6
7type BannerProps = {
8  text: string;
9  bgColor: string;
10};
11
12function Banner({ text, bgColor }: BannerProps) {
13  return (
14    <div
15      style={{
16        width: "100%",
17        height: "100dvh",
18        color: "black",
19        backgroundColor: bgColor,
20        display: "flex",
21        justifyContent: "center",
22        alignItems: "center",
23        fontSize: "4rem",
24      }}
25    >
26      {text}
27    </div>
28  );
29}
30
31export default function App() {
32  return (
33    <Swiper
34      slidesPerView={1}
35      modules={[Mousewheel]}
36      direction="vertical"
37      mousewheel={true}
38    >
39      <SwiperSlide>
40        <Banner text="Slide 1" bgColor="pink" />
41      </SwiperSlide>
42      <SwiperSlide>
43        <Banner text="Slide 2" bgColor="beige" />
44      </SwiperSlide>
45      <SwiperSlide>
46        <Banner text="Slide 3" bgColor="purple" />
47      </SwiperSlide>
48      <SwiperSlide>
49        <Banner text="Slide 4" bgColor="red" />
50      </SwiperSlide>
51
52      <SwiperSlide>
53        <Swiper
54          modules={[FreeMode, Mousewheel]}
55          freeMode={{ sticky: false, momentumRatio: 0.5 }}
56          direction="vertical"
57          mousewheel={true}
58          nested
59        >
60          <SwiperSlide>
61            <Banner text="Slide 5" bgColor="teal" />
62          </SwiperSlide>
63          <SwiperSlide>
64            <Banner text="Slide 6" bgColor="green" />
65          </SwiperSlide>
66          <SwiperSlide>
67            <Banner text="Slide 7" bgColor="aliceBlue" />
68          </SwiperSlide>
69        </Swiper>
70      </SwiperSlide>
71    </Swiper>
72  );
73}
74
75

Considerations

In this example, we use dvh to adjust the height of the slide. If your app is meant for newer browsers, this approach would be fine. However, some older browsers from iOS <14 or Android <9, won't parse this value.

One big drawback of this solution is when it's opened on an in-app-browser that might have a pull-down-to-close-window feature. In our case, the in-app-browser itself closes the window when the user scroll down as if they were pulling down the window. As in the design, each of the slides spans from the top to the bottom of the screen. During QA, I couldn't fix this behavior because this was from the browser itself. My solution was to adjust the behavior of the in-app-browser to stop closing the browser window when the user scrolls down too hard.