How to make a React component fade in on scroll using IntersectionObserver, but only once?

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));


    return () => observer.unobserve(domRef.current);
  }, []);

  return (
    <div ref={ domRef } className={ `fade-in-section ${ isVisible ? 'is-visible' : '' }` }>
      { props.children }

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;
} {
  opacity: 1;
  transform: none;
  visibility: visible;
  display: flex; 

Here is my website, which keeps fading components in and out, offering a terrible experience:

My Website

And this is the desired effect:

Sweet fade-in effect

How can I achieve the desired effect?

Here is a link to the code sandbox to test it: Code sandbox link

Here is Solutions:

We have many solutions to this problem, But we recommend you to use the first solution because it is tested & true solution that will 100% work for you.

Solution 1

You only need to call setVisible if entry.isIntersecting is true, so simply replace:



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 = ({
}) => {
  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:
        // No need to keep observing:
    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="[email protected]/umd/react.development.js"></script>
<script src="[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

Note: Use and implement solution 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from or, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply