Reputation: 778
I am trying to give components a fade-in effect in React when the user scrolls, but I want the fade-in effect to only happen the first time the element moves into the viewport.
Currently, the code I am using causes a fade-in every time the element moves into the viewport, so they are constantly fading in and out.
Here is my fade-in component:
import React, {useState, useRef, useEffect} from 'react';
import './styles/FadeInSection.css';
export default function FadeInSection(props) {
const [isVisible, setVisible] = useState(true);
const domRef = React.useRef();
useEffect(() => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => setVisible(entry.isIntersecting));
});
observer.observe(domRef.current);
return () => observer.unobserve(domRef.current);
}, []);
return (
<div ref={ domRef } className={ `fade-in-section ${ isVisible ? 'is-visible' : '' }` }>
{ props.children }
</div>
)
}
And these are the styles I'm using:
.fade-in-section {
opacity: 0;
transform: translateY(20vh);
isibility: hidden;
transition: opacity 0.2s ease-out, transform 0.6s ease-out;
will-change: opacity, visibility;
}
.fade-in-section.is-visible {
opacity: 1;
transform: none;
visibility: visible;
display: flex;
}
Here is my website, which keeps fading components in and out, offering a terrible experience:
And this is the desired effect:
How can I achieve the desired effect?
Here is a link to the code sandbox to test it: Code sandbox link
Upvotes: 14
Views: 22187
Reputation: 21161
You only need to call setVisible
if entry.isIntersecting
is true
, so simply replace:
setVisible(entry.isIntersecting);
With:
entry.isIntersecting && setVisible(true);
This way, once an entry has already been marked as visible, it won't be unmarked, even if you scroll back up, so the element goes out of the viewport, and entry.isIntersecting
becomes false
again.
Actually, you can even call observer.unobserve
at that point, as you don't care anymore.
const FadeInSection = ({
children,
}) => {
const domRef = React.useRef();
const [isVisible, setVisible] = React.useState(false);
React.useEffect(() => {
const observer = new IntersectionObserver(entries => {
// In your case there's only one element to observe:
if (entries[0].isIntersecting) {
// Not possible to set it back to false like this:
setVisible(true);
// No need to keep observing:
observer.unobserve(domRef.current);
}
});
observer.observe(domRef.current);
return () => observer.disconnect();
}, []);
return (<section ref={ domRef } className={ isVisible ? ' is-visible' : '' }>{ children }</section>);
};
const App = () => {
const items = [1, 2, 3, 4, 5, 6, 7, 8].map(number => (
<FadeInSection key={ number }>Section { number }</FadeInSection>
));
return (<main>{ items }</main>);
}
ReactDOM.render(<App />, document.querySelector('#app'));
body {
font-family: monospace;
margin: 0;
}
section {
padding: 16px;
margin: 16px;
box-shadow: 0 0 8px rgba(0, 0, 0, .125);
height: 64px;
opacity: 0;
transform: translate(0, 50%);
visibility: hidden;
transition: opacity 300ms ease-out, transform 300ms ease-out;
will-change: opacity, visibility;
}
.is-visible {
opacity: 1;
transform: none;
visibility: visible;
display: flex;
}
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>
Upvotes: 13