React make a full-page Carousel

Viewed 163

I am trying to make an animation that scrolls from left to right the child elements of a div, I have looked around but none of the other questions with answers seem to work, so far this is what I have accomplished:

Home.css:

@keyframes scroll {
    20% {
        transform: translateX(-100vw);
    }

    40% {
        transform: translateX(-200vw);
    }

    60% {
        transform: translateX(-300vw);
    }

    80% {
        transform: translateX(-400vw);
    }

    100% {
        transform: translateX(0vw);
    }
}

.section {
    display: flex;
    flex-direction: row;
    font-family: Arial, sans-serif;
    font-weight: bold;
    transition: 1s;
    width: 100vw;
    height: 100vh;
    animation-name: scroll;
    animation-duration: 1s;
}

.child1 {
    background-color: #c0392b;
    flex: none;
    width: 100vw;
    height: 100vh;
}

.child2 {
    background-color: #e67e22;
    flex: none;
    width: 100vw;
    height: 100vh;
}

.child3 {
    background-color: #27ae60;
    flex: none;
    width: 100vw;
    height: 100vh;
}

.child4 {
    background-color: #2980b9;
    flex: none;
    width: 100vw;
    height: 100vh;
}

.child5 {
    background-color: #8e44ad;
    flex: none;
    width: 100vw;
    height: 100vh;
}

Home.jsx:

import { forwardRef, useEffect, useRef, useState } from "react";
import { Link } from "react-router-dom";
import './Home.css';

const Child1 = forwardRef((props, ref) => {
    return <div className="child1">
        <h1>Child1</h1>
    </div>;
});

const Child2 = forwardRef((props, ref) => {
    return <div className="child2">
        <h1>Child2</h1>
    </div>;
});

const Child3 = forwardRef((props, ref) => {
    return <div className="child3">
        <h1>Child3</h1>
    </div>;
});

const Child4 = forwardRef((props, ref) => {
    return <div className="child4">
        <h1>Child4</h1>
    </div>;
});

const Child5 = forwardRef((props, ref) => {
    return <div className="child5">
        <h1>Child5</h1>
    </div>;
});

function Home() {

    let section = useRef();
    let [currentSection, setCurrentSection] = useState(0);

    useEffect(() => {
        window.addEventListener("keypress", (event) => {
            if(event.key === "d") {
                // scroll right (play animation forwards by one step)
            } else if(event.key === "a") {
                // scroll left (play animation backwards by one step)
            }
        });
    });

    return <div ref={section} className="section">
        <Child1></Child1>
        <Child2></Child2>
        <Child3></Child3>
        <Child4></Child4>
        <Child5></Child5>
    </div>
}

export default Home;

The problem is that the animation plays all at the beginning and I cannot figure out a way to play it only when the keyboard event is triggered, if the event.key is an "a" then the elements should scroll to the left otherwise if the event.key is a "d" then the elements should scroll to the right.

Here is the link to the CodeSandbox.

2 Answers

As I understood you want to make parent scrollable only with pressing keys "a" and "d". I think I have found a solution which would work for you.

My solution:

Removing keyframes and separating it into 5 different classes. So here is CSS file:

.section1 {
    transform: translateX(0);
}
.section2 {
    transform: translateX(-100vw);
}
.section3 {
    transform: translateX(-200vw);
}
.section4 {
    transform: translateX(-300vw);
}
.section5 {
    transform: translateX(-400vw);
}
.section {
    display: flex;
    flex-direction: row;
    font-family: Arial, sans-serif;
    font-weight: bold;
    transition: 1s;
    width: 100vw;
    height: 100vh;
    animation-name: scroll;
    animation-duration: 1s;
}

.child1 {
    background-color: #c0392b;
    flex: none;
    width: 100vw;
    height: 100vh;
}

.child2 {
    background-color: #e67e22;
    flex: none;
    width: 100vw;
    height: 100vh;
}

.child3 {
    background-color: #27ae60;
    flex: none;
    width: 100vw;
    height: 100vh;
}

.child4 {
    background-color: #2980b9;
    flex: none;
    width: 100vw;
    height: 100vh;
}

.child5 {
    background-color: #8e44ad;
    flex: none;
    width: 100vw;
    height: 100vh;
}

Now I added some JS behaviour to appropriately 'scroll' left or right. I divided moving left or right into functions, and adding numberOfSections as constant. Here is JS file:

import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
import { Link } from "react-router-dom";
import "./Home.css";

const Child1 = forwardRef((props, ref) => {
  return (
    <div className="child1">
      <h1>Child1</h1>
    </div>
  );
});

const Child2 = forwardRef((props, ref) => {
  return (
    <div className="child2">
      <h1>Child2</h1>
    </div>
  );
});

const Child3 = forwardRef((props, ref) => {
  return (
    <div className="child3">
      <h1>Child3</h1>
    </div>
  );
});

const Child4 = forwardRef((props, ref) => {
  return (
    <div className="child4">
      <h1>Child4</h1>
    </div>
  );
});

const Child5 = forwardRef((props, ref) => {
  return (
    <div className="child5">
      <h1>Child5</h1>
    </div>
  );
});

const numberOfSections = 5;

function Home() {
  const section = useRef();
  const [currentSection, setCurrentSection] = useState(1);

  const moveLeft = () => {
    section.current.classList.remove(`section${currentSection}`);
    section.current.classList.add(`section${currentSection - 1}`);
    setCurrentSection((prevSection) => prevSection - 1);
  };

  const moveRight = () => {
    section.current.classList.remove(`section${currentSection}`);
    section.current.classList.add(`section${currentSection + 1}`);
    setCurrentSection((prevSection) => prevSection + 1);
  };

  const changeSection = (event) => {
    if (event.key === "d") {
      if (currentSection < numberOfSections) {
        moveRight();
      }
    } else if (event.key === "a") {
      if (currentSection > 1) {
        moveLeft();
      }
    }
  };

  useEffect(() => {
    window.addEventListener("keypress", changeSection);
    return () => window.removeEventListener("keypress", changeSection);
  }, [currentSection]);

  return (
    <div ref={section} className="section">
      <Child1></Child1>
      <Child2></Child2>
      <Child3></Child3>
      <Child4></Child4>
      <Child5></Child5>
    </div>
  );
}

export default Home;

I have tested it and it works.

By the way, my suggestions is to wrap some of these functions into useCallback if you plan to expand Home component. And I suggest to use some of npm packages compatible with React for directly manipulating CSS within JS, such as styled-components, which in this case would make it a lot easier to create Carousel, becouse now in order to create more Child components, you need to make new CSS classes.

But this above fixes the given problem. I hope I helped you :)

this solution is tightly cuppled to your example but from my perspective, you could make it more generic.

I didn't use CSS animation instead I used the scrollTo method to achieve scroll behavior.

codesandbox link

Home.jsx

import { forwardRef, useEffect, useRef, useState } from "react";
import "./styles.css";

//  I not using forwardRef and ref at all
const Child1 = forwardRef((props, ref) => {
  return (
    <div className="child1">
      <h1>Child1</h1>
    </div>
  );
});

const Child2 = forwardRef((props, ref) => {
  return (
    <div className="child2">
      <h1>Child2</h1>
    </div>
  );
});

const Child3 = forwardRef((props, ref) => {
  return (
    <div className="child3">
      <h1>Child3</h1>
    </div>
  );
});

const Child4 = forwardRef((props, ref) => {
  return (
    <div className="child4">
      <h1>Child4</h1>
    </div>
  );
});

const Child5 = forwardRef((props, ref) => {
  return (
    <div className="child5">
      <h1>Child5</h1>
    </div>
  );
});

function App() {
  let section = useRef();
  let [currentSection, setCurrentSection] = useState(0);

  useEffect(() => {
    const handler = (event) => {
      if (event.key === "d") {
        if (currentSection === 4) return;

        setCurrentSection((prev) => prev + 1);
      } else if (event.key === "a") {
        if (currentSection === 0) return;

        setCurrentSection((prev) => prev - 1);
      }
    };

    window.addEventListener("keypress", handler);

    // you should clean up you EventListener
    return () => {
      window.removeEventListener("keypress", handler);
    };
  }, [currentSection]);

  useEffect(() => {
    window.scrollTo({
      behavior: "smooth",
      left: window.innerWidth * currentSection,
      top: 0,
    });
  }, [currentSection]);

  return (
    <div ref={section} className="section">
      <Child1></Child1>
      <Child2></Child2>
      <Child3></Child3>
      <Child4></Child4>
      <Child5></Child5>
    </div>
  );
}

export default App;

Home.css

.section {
    display: flex;
    flex-direction: row;
    font-family: Arial, sans-serif;
    font-weight: bold;
    transition: 1s;
    width: 100vw;
    height: 100vh;
}

.child1 {
    background-color: #c0392b;
    flex: none;
    width: 100vw;
    height: 100vh;
}

.child2 {
    background-color: #e67e22;
    flex: none;
    width: 100vw;
    height: 100vh;
}

.child3 {
    background-color: #27ae60;
    flex: none;
    width: 100vw;
    height: 100vh;
}

.child4 {
    background-color: #2980b9;
    flex: none;
    width: 100vw;
    height: 100vh;
}

.child5 {
    background-color: #8e44ad;
    flex: none;
    width: 100vw;
    height: 100vh;
}
Related