I’m trying to create a testimonial section similar to this (www.runway.com) website’s testimonials. I am using styled-components to create it. So far I have managed to stack up the cards on top of each another at the middle of the screen and when it comes into view, the cards rise from the bottom of the screen to the middle with a linear animation. But I want a scroll feature here, (like the website mentioned above) where the cards would come from the bottom of the screen one-by-one when the user gradually scrolls down.
//
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import ticket from "./assets/ticket0.webp";
const TestimonialSection = styled.section``;
const TestimonialDiv = styled.div`
height: 80vh;
margin: auto;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
min-height: 70%;
`;
const BottomCenter = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
@keyframes splash {
from {
transform: translate(0%, 100%);
}
to {
transform: rotate(var(--rx));
}
}
img{
width: 0%;
position:absolute;
transform:rotate(0deg);
}
.f-image-25{
width:15%;
--rx: -3deg;
z-index:25;
animation: splash 1s normal forwards ease-in-out;
}
.f-image-35{
width:17%;
--rx: -3deg;
z-index:35;
animation: splash 1s normal forwards ease-in-out;
}
.f-image-40{
width:20%;
--rx: 3deg;
z-index:40;
animation: splash 1s normal forwards ease-in-out;
}
.f-image-30{
width:17%;
--rx: -6deg;
z-index:30;
animation: splash 1s normal forwards ease-in-out;
}
.f-image-20{
width:15%;
--rx: 10deg;
z-index:20;
animation: splash 1s normal forwards ease-in-out;
}
`;
const Testimonial = () => {
const [isAnimated, setIsAnimated] = useState(false);
const options = {
root: null,
margin: '0px',
threshold: 0.5
}
const observerCallback = (entries) => {
const [mockImg] = entries
if (!isAnimated) {
setIsAnimated(true);
if (mockImg.isIntersecting) {
document.getElementById('f-image-40').classList.add('f- image-40')
document.getElementById('f-image-35').classList.add('f-image-35')
document.getElementById('f-image-30').classList.add('f-image-30')
document.getElementById('f-image-25').classList.add('f-image-25')
document.getElementById('f-image-20').classList.add('f-image-20')
}
}
}
useEffect(() => {
const fImage = document.getElementById('f-image')
const observer = new IntersectionObserver(observerCallback, options)
observer.observe(fImage)
return () => {
observer.unobserve(fImage);
}
}, [])
return (
<TestimonialSection>
<TestimonialDiv>
<BottomCenter id='f-image'>
<img alt="testimonial-card" id='f-image-25' src={ticket}></img>
<img alt="testimonial-card" id='f-image-35' src={ticket}></img>
<img alt="testimonial-card" id='f-image-40' src={ticket}></img>
<img alt="testimonial-card" id='f-image-30' src={ticket}></img>
<img alt="testimonial-card" id='f-image-20' src={ticket}></img>
</BottomCenter>
</TestimonialDiv>
</TestimonialSection>
)
}
export default Testimonial
I would recommend using the Framer library for React.
You can use a combination of the useInView hook with the animate function. Framer sounds very suitable for your use case as it comes equipped with multiple hooks and functions to handle these scenarios.
https://www.framer.com/motion/animate-function/
https://www.framer.com/motion/use-in-view/
You’ll need to use react-intersection-observer. You’d use the
useInView
hook to figure out if the card(s) in view and then pass that as a prop to each card, conditionally invoking the fade animations