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
Vanilla CSS
Styled Components
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
Create a Loader Component
We are going to use the
reack-hook-inview
to observe if the loader is in the viewportIf 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:
- Add Auto-play with Intersection Observer API