Follow

Follow
Create Tik-Tok/Youtube Shorts like snap infinite scroll - React

Create Tik-Tok/Youtube Shorts like snap infinite scroll - React

Pratik Sharma's photo
Pratik Sharma
ยทMay 23, 2023ยท

5 min read

Play this article

Table of contents

  • Vanilla CSS
  • Styled Components
  • React Hooks

Scroll snap is when you scroll a little and it auto scrolls to the next card in the list. You must have seen this feature on Instagram, youtube shorts and TikTok.

Snap Scroll can be achieved by CSS only. https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-type

There are three ways to achieve this effect in react

  1. Vanilla CSS

  2. Styled Components

  3. React-hook

Browser Compatibility of scroll-snap-type is great. All the browsers have stable support for it.

Before you can define scroll snapping, you need to enable scrolling on a scroll container. You can do this by ensuring that the scroll container has a defined size and that it has overflow enabled.

You can then define scroll snapping on the scroll container by using the following two key properties:

  • scroll-snap-type: Using this property, you can define whether or not the scrollable viewport can be snapped to, whether snapping is required or optional, and the axis on which the snapping should occur.

  • scroll-snap-align: This property is set on every child of the scroll container and you can use it to define each child's snap position or lack thereof.

    • (Using the scroll-snap-stop property, you can ensure that a child is snapped to during scrolling and not passed over.

    • scroll-margin properties on child elements that are snapped to during scrolling to create an outset from the defined box.) [Optional]

    • Optional scroll-padding properties can be set on the scroll container to create a snapping offset. [Optional]

Vanilla CSS

The scroll-snap-type CSS property sets how strictly snap points are enforced on the scroll container if there is one.

For the snap-scroll to work properly, we will have a Container and Children. the container will have the scroll-snap-type CSS property and the Children will have the scroll-snap-align CSS property.

scroll-snap-type: x mandatory;
scroll-snap-type: y proximity;

x and y will constrain the scroll-snap action to the x-axis and y-axis.

Mandatory means the scroll will always rest on the scroll-snap point( video component ).

Proximity is based on the proximity of the scroll-snap point/Children. Scroll container may come to rest on a snap point if it isn't currently scrolled considering the user agent's scroll parameters

Let's start with simple HTML structure.


<!-- Scroll Container Component  -->
<div class="container" dir="ltr">
  <!-- List Component  -->
  <div id="list">
  </div>
  <!-- Loader Component  -->
  <p id="watch_end_of_document"  class="loader">
  Loader ...
</p>
</div>
.container {
   height: 100vh;
   scroll-snap-type: y mandatory;
   overflow: scroll;
}


.item {
  margin: 0;
  padding: 20px 0;
  text-align: center;
  scroll-snap-align: start;
  height: 300px;
}

.loader {
  height: 50px;
  display: flex;
  background: #eee;
  justify-content: center;
}


.item:nth-child(even) {
  background: #eee;
}

How does infinite scroll work?

Whenever the loader component comes into view, we run a fetch function.

// Get the Loader Component
const loader = document.getElementById("loader")

// addItems - can also be fetch function
function addItems() {
  const fragment = document.createDocumentFragment();

  for (let i = index + 1; i <= index + count; ++i) {
    const item = document.createElement("p");

    item.classList.add("item");
    item.textContent = `#${i}`;

    fragment.appendChild(item);
  }
  document.getElementById("list").appendChild(fragment);
  index += count;
}

// Using the IntersectionObserver API we will observe the loader component 
// if the 
const io = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    // componet is not in view we do nothing 
    if (!entry.isIntersecting) {
      return;
    }
    console.log('this is working');
    addItems();
  });
});

io.observe(loader);

Styled Components

Let's use Styled Components in React to do the same. Starting with the list Container Component which would have scroll-snap-type and overflow-y .

import styled from "styled-components";

const List = styled.div`
  max-height: 100vh;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  background: #fff;
  display: flex;
  flex-direction: column;
  gap: 20px;
  scrollbar-width: none;

  &::-webkit-scrollbar {
    display: none; /* for Chrome, Safari, and Opera */
  }
`;

I used &::-webkit-scrollbar to hide the scrollbar.

Now, the Item component would have the scroll-snap-align CSS property.

const Item = styled.div`
  margin: 0;
  padding: 20px 0px;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
  align-content: center;
  scroll-snap-align: start;
  min-height: 70vh;

  background: #eee;
`;

Now, to we are done with the scroll-snap.

Infinite scroll with styled-component

  1. Create a Loader Component

  2. We are going to use the reack-hook-inview to observe if the loader is in the viewport

  3. If the Loader is in the viewport, fetch more items.

const Loader = styled.div`
  min-height: 20vh;
  margin-bottom: 30px;
  display: flex;
  background: #444;
  scroll-snap-align: start;
  color: #eee;
  align-content: center;
  align-items: center;
  scroll-snap-align: start;

  justify-content: center;
`;
function ScrollContainer() {
  const [state, setState] = useState([1, 2, 3, 4, 5]);
  return (
    <List>
      {state.map((el, index) => (
        <Item key={index + el}>{el} </Item>
      ))}
      <Loader >Loading...</Loader>
    </List>
  );
}
export default ScrollContainer;

react-hook-inview provides useInView hook, which we can use to observe the loader component.

We are going to use useEffect , if the Loader is in the viewport. We Fetch more data.

function ScrollContainer() {
... 

const [ref, isVisible] = useInView({
    threshold: 1
  });

  const newData = [...Array(10).keys()].map((x) => x + state.length + 1);

  useEffect(() => {
    if (isVisible) {
      setState((state) => [...state, ...newData]);
    }
  }, [isVisible]);

  return (
    <List>
      {state.map((el, index) => (
        <Item key={index + el}>{el} </Item>
      ))}
      <Loader ref={ref}>Loading...</Loader>
    </List>
  );
}

That's it ๐Ÿ”ฅ . Here is CodeSandbox with all the code

React Hooks

We are going to use react-use-scroll-snap . react-use-scroll-snap Give us simple API and keyboard accessibility as well. It uses the tweezer.js library.

import useScrollSnap from "react-use-scroll-snap";
import { useRef } from "react";

function ScrollComponent() {
  const scrollRef = useRef(null);
  const data = Array(10)
        .fill(1)
        .map((i, e) => e + 1);

  useScrollSnap({ ref: scrollRef, duration: 100, delay: 50 });


  return (
    <section className="container" ref={scrollRef}>
      {data.map((el, index) => (
        <div key={el} className="item">
          {el}
        </div>
      ))}
    </section>
  );
}

See, less code ๐Ÿ™Œ๐Ÿป. Here is the code Sandbox for you to fork it. Try to add a loader component, add a fetch function and useEffect to call the fetch function.

Further Improvement:

  1. Add Auto-play with Intersection Observer API

Reference:

  1. https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scroll_Snap/Basic_concepts

  2. https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scroll_Snap

  3. https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-type#try_it

Did you find this article valuable?

Support Pratik Sharma by becoming a sponsor. Any amount is appreciated!

See recent sponsors |ย Learn more about Hashnode Sponsors
ย 
Share this