I am working on a React project where I have components animate in when they scroll in to view. I am using Framer Motion. How can I make it so the animation only triggers the first time you scroll by the component?
Right now, if I scroll down the page the animations work as expected. However, if I refresh or leave the page and come back the animations will fire again. Scrolling to the middle of the page, refreshing, and then scrolling back up will fire animations on components that were scrolled by before.
I understand this is the default behavior of Framer Motion going from initial value to animate value as components remount. I am looking to prevent this behavior on components that where in the users viewport before.
Sample code for one of the components is posted below. Any help is appreciated.
const Banner = ({ title, body, buttonStyle, buttonText, image, switchSide, link }) => {
const { ref, inView } = useInView({
threshold: .8
})
return (
<motion.div className="banner"
ref={ref}
initial={{ opacity: 0 }}
animate={ inView ? { opacity: 1 } : ''}
transition={{ duration: .75 }}
>
<div className={`container ${switchSide ? 'banner-switch': ''}`}>
<div className="side-a">
<img src={ image } />
</div>
<div className="side-b">
<h2>{ title }</h2>
<p>{ body }</p>
{
buttonText
? <Button buttonStyle={buttonStyle} link={link} justify="flex-start">{ buttonText }</Button>
: ''
}
</div>
</div>
</motion.div>
)
}
export default Banner
I was faced with a similar problem lately. I was implementing intro animation and did not want it to trigger on every page refresh, so I have made a custom hook which saves a timestamp in local storage and on each page refresh compares saved time with stored timestamp and fires when the time has passed and stores a new value there. If you want to play it only once you could simply implement customize my code to store boolean and you’re good to go.
My custom hook
import {useEffect} from 'react';
const useIntro = () => {
const storage = window.localStorage;
const currTimestamp = Date.now();
const timestamp = JSON.parse(storage.getItem('timestamp') || '1000');
const timeLimit = 3 * 60 * 60 * 1000; // 3 hours
const hasTimePassed = currTimestamp - timestamp > timeLimit;
useEffect(() => {
hasTimePassed ?
storage.setItem('timestamp', currTimestamp.toString())
:
storage.setItem('timestamp', timestamp.toString());
}, []);
return hasTimePassed;
};
export default useIntro;
You would need to make this simple change in your code
const Banner = ({ title, body, buttonStyle, buttonText, image, switchSide, link }) => {
const showAnimation = useIntro();
const { ref, inView } = useInView({
threshold: .8
})
return (
<motion.div className="banner"
ref={ref}
initial={{ opacity: 0 }}
animate={ inView && showAnimation ? { opacity: 1 } : ''}
transition={{ duration: .75 }}
>
<div className={`container ${switchSide ? 'banner-switch': ''}`}>
<div className="side-a">
<img src={ image } />
</div>
<div className="side-b">
<h2>{ title }</h2>
<p>{ body }</p>
{
buttonText
? <Button buttonStyle={buttonStyle} link={link} justify="flex-start">{ buttonText }</Button>
: ''
}
</div>
</div>
</motion.div>
)
}
export default Banner
Hope this is what you were after.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With