Search code examples
javascriptcssreactjsscrollsticky

How to switch image on scroll in React (like on Apple website)?


I try to switch from an image to another one when scrolling with React but I don't know how to do this... maybe with React hook. I know it's possible with jQuery but I don't want to use it.

Example here: https://www.apple.com/iphone-12/, look at the "Five fresh finishes".

Apple

import React, {useState} from 'react';
import './BackgroundDiscover.css';
import logoOrdiW from "../../images/mathis_computer_white.jpg";
import logoOrdiB from "../../images/mathis_computer_black.jpg";


const BackgroundDiscover = () => {
    const [imageSrc, SetSrc] = useState(logoOrdiW);
    const switchBrackground = () => {
        SetSrc(logoOrdiB);
    }
    return (
        <>
            <div className="container-discover">
                <div className='container-logo-white'>
                    <img src={imageSrc} alt="logo ordinateur mathis blanc" onScroll={switchBrackground}/>
                </div>
            </div>
        </>
    );
};



export default BackgroundDiscover;
.container-discover {
    display: flex;
    flex-direction: column;
}

.container-logo-white img {
    width: 100%;
}

.container-logo-black img {
    width: 100%;
}

Solution

  • You don't need jQuery or something like this. You could subscribe to scroll event in useEffect() hook with addEventListener and check scrollTop of a scrolling container.

    If you want to do the same effect as the Apple website has, you could add some magic with position: sticky, height in vh units and checking window.innerHeight.

    Note that this code is simplified, not optimized, and only needed to understand the idea:

    CodeSandbox

    index.js:

    import { useLayoutEffect, useState } from "react";
    import { render } from "react-dom";
    import classnames from "classnames";
    import "./index.css";
    
    const images = [0, 1, 2, 3, 4];
    
    const App = () => {
      const [visibleImagesMap, setVisibleImagesMap] = useState(
        images.reduce((map, image) => {
          map[image] = false;
          return map;
        }, {})
      );
    
      useLayoutEffect(() => {
        const handleScroll = () => {
          const scrollTop = document.documentElement.scrollTop;
          const viewportHeight = window.innerHeight;
    
          const newVisibleImagesMap = images.reduce((map, image) => {
            map[image] = scrollTop >= image * viewportHeight;
            return map;
          }, {});
    
          setVisibleImagesMap(newVisibleImagesMap);
        };
    
        window.addEventListener("scroll", handleScroll);
        handleScroll();
    
        return () => window.removeEventListener("scroll", handleScroll);
      }, []);
    
      return (
        <div className="app">
          <div className="sticky">
            <div className="frame">
              {images.map((image) => (
                <div
                  className={classnames("image", `image_${image}`, {
                    image_visible: visibleImagesMap[image]
                  })}
                  key={image}
                />
              ))}
            </div>
          </div>
        </div>
      );
    };
    
    render(<App />, document.getElementById("root"));
    

    index.css:

    body {
      margin: 0;
    }
    
    .app {
      height: 500vh;
    }
    
    .sticky {
      position: sticky;
      top: 0;
      height: 100vh;
    }
    
    .frame {
      z-index: 1;
      position: relative;
      height: 100%;
      width: 100%;
    }
    
    .image {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      opacity: 0;
      background-repeat: no-repeat;
      background-size: cover;
    }
    
    .image_0 {
      z-index: 0;
      background-image: url("./images/0.jpeg");
    }
    
    .image_1 {
      z-index: 1;
      background-image: url("./images/1.jpeg");
    }
    
    .image_2 {
      z-index: 2;
      background-image: url("./images/2.jpeg");
    }
    
    .image_3 {
      z-index: 3;
      background-image: url("./images/3.jpeg");
    }
    
    .image_4 {
      z-index: 4;
      background-image: url("./images/4.jpeg");
    }
    
    .image_visible {
      opacity: 1;
    }