DEV Community

loading...

Coding a React Carousel From Scratch

nicklevenson
I'm a newer full stack engineer who is here to learn and write about my coding endeavors. Let's get in touch!
・4 min read

I have recently been working on an app for musicians to connect and be matched up with based on similar preferences. I wanted the UX/UI to be something like a tinder swiper where you can scroll through different profile cards. In the past I have used libraries like Bootstrap to achieve the carousel-like presentation, however, I wanted to challenge myself to build that out myself with vanilla JS within my React app.

My first thoughts were to use CSS animation with toggled classes to move the cards in and out of the screen, however, I quickly found this method ineffective. I soon knew I would have to use JS to solve this problem. So allow me to walk you through my process.

To start, I needed to have an array of data - recommended musicians to swipe through.This was relatively easy given I had stored those recommendations in my Redux state. Note, you don’t need Redux for this, I am just using it because I have redux implemented for the larger context of my application. All you really need is an array to map over.

For Redux, all I had to do was map my state to props in the recommended users component like so:

const mapStateToProps = (state) => {
  return {
    currentUser: state.currentUser.currentUser,
    recommendedUsers: state.currentUser.recommendedUsers,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    fetchUserRecs: () => dispatch(fetchUserRecs()),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(RecommendedUsers);
Enter fullscreen mode Exit fullscreen mode

I mapped my the fetch recommended users dispatch function to props as well so when this component mounted in the application, it would fetch this data.

Now was the time where I had to figure out how to actually implement the carousel-like behavior. After some experimentation, I decided that I would make the container for all the recommended user profiles to be a div that had an overflow hidden property, with a nowrap white-space property. This meant that the div could not break its line and would continue horizontally. I could then manipulate the scrollLeft margin of the container with JS to shift what is in view based on what card is shown. This is what the CSS looked like for the cards-container div, as well as the card class itself:

.cards-container {
    height: 100%;
    overflow: hidden;
    white-space: nowrap;
  }

 .card {
    display: inline-block;
    width: 100%;
    height: 100%;
    padding: 1rem;
  }
Enter fullscreen mode Exit fullscreen mode

Next I had to define some state variables locally in the component itself. I needed to figure out what the index in the array of recommended users of the active card was, so that would be a variable. And then I need a variable to store the current scroll margin to implement. So my component state looked like this:

state = {
    activeIndex: 0,
    margin: 0,
 };
Enter fullscreen mode Exit fullscreen mode

My render function looked something like this:

 const shownUserId = this.props?.recommendedUsers[this.state.activeIndex]?.id || null;
      return (
        <div className="recommended-users">
          <div className="cards-container">
            {this.props?.recommendedUsers?.map((u, index) => (
              <div>
                <PreviewUserCard
                  user={u}
                  currentUser={this.props.currentUser}
                  key={u.id}
                  cardChange={this.cardChange}
                  shownUserId={shownUserId}
                />
              </div>
            ))}
          </div>
        </div>
      );

Enter fullscreen mode Exit fullscreen mode

Basically I was mapping a component called PreviewUserCard that rendered all a user's information for each user in the recommended array. I passed in a callback function called cardChange that could be executed within the PreviewUserCard component. In the PreviewUserCard there is a button for the user to click that triggers this callback. This function is what would control the scrollLeft margin and change the active index.

  cardChange = (e) => {
    if (this.state.activeIndex === this.props.recommendedUsers.length - 1) {
      this.setState({ activeIndex: 0 });
      this.setState({ margin: 0 });
    } else {
      this.setState((state) => ({
        activeIndex: state.activeIndex + 1,
        margin: state.margin + window.innerWidth
      }));

    }
  };

Enter fullscreen mode Exit fullscreen mode

Basically, this function first checks if the current activeIndex is at the end of the recommended users array, and if it is, resets the active index to the first card - 0, as well as sets the margin to 0 as well. Otherwise, it will increment the activeIndex by 1 to the next user in the array and set the margin to the window width in addition to the previous margin. This is because a card is the width of the window and by increasing the scrollLeft margin by 100% we are essentially displaying the next card in the div.

The last part of this puzzle is the incrementally set the scrollLeft value. If we changed it all at once, there would be no carousel effect at all. So I decided to write a function that would be executed whenever the component updated (it will execute whenever the cardChange function is called). This important function is called setMargin, which essentially increments the current scrollLeft value in smaller chunks to give it a nice flow and feeling of swiping. It looks like this:

  setMargin = () => {
    const container = document.querySelector(".cards-container");
    let interval = setInterval(() => {
      let i = container.scrollLeft;
      if (i < this.state.margin) {
        container.scrollLeft = i + window.innerWidth / 100;
        if (container.scrollLeft >= this.state.margin) {
          clearInterval(interval);
        }
      } else {
        container.scrollLeft = i - window.innerWidth / 50;
        if (container.scrollLeft <= this.state.margin) {
          clearInterval(interval);
        }
      }
    }, 1);
  };

Enter fullscreen mode Exit fullscreen mode

First we get the cards container element and set that to a variable. Then, we set an interval which takes the current value of that container scrollLeft margin. It then says, while this current scroll value is less than the component state's margin value (our target value), increment in small amounts the current scrollLeft value until we hit out target scrollLeft value and then clear the interval. If the current scroll value of the container is MORE than our target value, then that means we have reached the end of our array and have reset to 0. We then do a similar thing of changing the current scroll value until we hit our target, however this time we are decrementing down (and doing it faster for a nice effect).

And that's it! If you've successfully followed along, you now know how to implement this yourself. There probably is a better way to do this, and I would love to implement touch events and smoothing incrementation (now it is all linear so it could be more natural), but for now I am proud to have came up with this method. It would probably be faster to just use a library like React-Carousel, or Bootstrap's Carousel, but this was a fun and enjoyable challenge. Feel free to comment any other solutions you may have to creating a carousel-like presentation. Thanks for reading!

Discussion (5)

Collapse
ivanjeremic profile image
Ivan Jeremic • Edited

Redux for this? 🙁😳

Collapse
nicklevenson profile image
nicklevenson Author

Redux is totally not required! You really just need an array of something to map divs in the parent container. I happened to be using redux in the larger context of my bigger application with this component.

Collapse
keefdrive profile image
Keerthi

Its a nice start on how to hand code a carousel. Obviously in the end product you will have considerations for accesibilty, responsiveness and all the other edge cases too.

Collapse
oenonono profile image
Junk

How is this accessible? For example, to people who need longer to view a particular item. Or to screen readers?

Collapse
nicklevenson profile image
nicklevenson Author • Edited

That’s a great question. Well I didn’t mention this, but I am using a button element for people to hit ‘Next’ with so the carousel doesn’t auto play at all. It’s not perfect by any means, and I’m not done building the final product either - so I will definitely need to look into how to make it more accessible than it is currently - I think screen readers will be the biggest challenge in that light.