photo of stacked rocks on beach
Photo by Stephen Walker on Unsplash

Building a React Card Carousel Component

In this post we will explore creating a card carousel like the one seen on Stripe.com.

7 min read

In this article we'll build a card carousel UI like the one seen on Stripe.com.

example of Stripe's card carousel
example of Stripe's card carousel


This carousel displays its content as a set of cards that animate off the top of a "stack" when the next item becomes active. It's a bit different than other carousel treatments, but all we need is some positioning and transition trickery to build it. Let's get started!

Before adding any markup, we should first define how the card carousel will work.

  1. To begin with, we need a number of cards to display some data. We will add this data right into our component, but in the real world, it would be coming from somewhere else, likely a data prop.
  2. We know that we want to transition through the carousel cards automatically after an interval of a few seconds.
  3. We'll need to keep track of a few different pieces of state to know which item is active, which one was previously shown, and which one is up next.

Markup and State

Starting with the layout, let's first create a new React component named CardCarousel.jsx.


import React, { useEffect, useCallback, useState } from 'react';

const CardCarousel = () => {
  const [indexes, setIndexes] = useState({
    previousIndex: 0,
    currentIndex: 0,
    nextIndex: 1,
  });

  return ();
}

In this component, the useState hook will keep track of our state. Above we identified that we want to maintain the previous item, current item and the next item. We'll do that by storing the item indexes.

We've also imported useCallback and useEffect, which we'll go over later.

Next let's add in some sample data for the carousel cards.


const cardItems = [
  {
    id: 1,
    title: 'Stacked Card Carousel',
    copy: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sit amet dui scelerisque, tempus dui non, blandit nulla. Etiam sed interdum est.',
  },
  {
    id: 2,
    title: 'Second Item',
    copy: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
  },
  {
    id: 3,
    title: 'A Third Card',
    copy: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sit amet dui scelerisque, tempus dui non, blandit nulla.',
  },
  {
    id: 4,
    title: 'Fourth',
    copy: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
  },
];

const CardCarousel = () => { ... }

Now for the markup.

When working through this example, we'll be using a button to manually transition the carousel to the next item. We'll remove that at the end, but it's helpful for debugging purposes to be able to control the carousel state manually.


return (
  <div className="container">
    <button onClick={handleCardTransition}>Transition to Next</button>
    <ul className="card-carousel">
      {cardItems.map((card, index) => (
        <li
          key={card.id}
          className={`card ${determineClasses(indexes, index)}`}
        >
          <h2>{card.title}</h2>
          <p>{card.copy}</p>
        </li>
      ))}
    </ul>
  </div>
);

We create the temporary button to move between carousel cards, and an unordered list that contains our card items, making sure each item has a key using the card.id value.

Additionally, we use a function determineClasses that takes the state index data and the current card index to determine which CSS class should be applied to each card at any given time.


function determineClasses(indexes, cardIndex) {
  if (indexes.currentIndex === cardIndex) {
    return 'active';
  } else if (indexes.nextIndex === cardIndex) {
    return 'next';
  } else if (indexes.previousIndex === cardIndex) {
    return 'prev';
  }
  return 'inactive';
}

const CardCarousel = () => { ... }

By using the indexes state data, we can easily determine which card was shown previously, which should be active, and which one is up next just by comparing the corresponding index values against the cards index.

You might be wondering why we need to track the previous and next indexes at all. This is due to the styling treatment. The carousel shows a part of the card that will display next and we need a way to target it. We could possibly accomplish this part with some clever CSS selectors. However, when we get to the end of our carousel cards (index 3 of 3, for example), we want to be able to seamlessly transition back to the first card (index 0), which is now the next card. I could not find a way to do this outside of maintaining a nextIndex state and using that to determine what the "next" value should be.>> The previousIndex value is needed for basically the inverse reason. If we have moved through the cards, and are starting at the first item again (index 0), then the previous value would have been the last item in the carousel.>> This is a little easier to visualize once the next part is completed, and you can see the classes being added and removed in the DOM.

Finally, we need to add the logic that handles the state updates.


const handleCardTransition = useCallback(() => {
  // If we've reached the end, start again from the first card,
  // but carry previous value over
  if (indexes.currentIndex >= cardItems.length - 1) {
    setIndexes({
      previousIndex: cardItems.length - 1,
      currentIndex: 0,
      nextIndex: 1,
    });
  } else {
    setIndexes((prevState) => ({
      previousIndex: prevState.currentIndex,
      currentIndex: prevState.currentIndex + 1,
      nextIndex: prevState.currentIndex + 2 === cardItems.length ? 0 : prevState.currentIndex + 2,
    }));
  }
}, [indexes.currentIndex]);

We are using a useCallback hook above. This means that this function is memoized and won't change unless the indexes.currentIndex value changes. It allows us to use this function without moving it inside of the useEffect hook that we'll add later.

The logic is split based on if we are at the end of the carousel or not. If we aren't, we set the previousIndex value to our currentIndex value and we increment currentIndex by 1.

Setting the nextIndex is slightly more involved. We can use a ternary operator to check if adding 2 to the currentIndex will put us at the end of the carousel items. If it does, then the next item is actually the first item in our carousel. Otherwise, we add 2.

card carousel progress
card carousel progress


With all of this out of the way, if you click through the carousel, you should see the correct classes being applied.

The carousel state is fully functioning now, so next we can add our styles.

Styling

Let's start by adding the card-carousel styles. Most of these these are for alignment purposes to center the cards, as well as to remove any unnecessary bullets and padding.


.card-carousel {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  height: 200px;
  margin: 100px auto;
  align-items: center;
  position: relative;
}

Next, the styles for the carousel cards.


.card {
  background: #ffffff;
  border-radius: 8px;
  border: 1px solid #eeeeee;
  padding: 30px;
  box-shadow: 0 10px 5px rgba(0, 0, 0, 0.1);
  width: 420px;
  height: 200px;
  transition: all 0.75s ease;
  opacity: 0;
  position: absolute;
  transform: scale(0.85) translateY(50px);
}

.card h2,
.card p {
  line-height: 1.5;
  margin: 0;
}

The styles here are pretty straightforward, but there are a few things worth mentioning.

  1. The height and width for the cards are specifically defined. The values are arbitrary for this example and could be set to anything, but should probably be set to something.
  2. We have the opacity for all cards defaulted to 0. Active and next cards are the only ones we want to be visible.3. All cards should be scaled to a slightly smaller value (0.85) and translated downwards 50px by default.

Now let's tackle the active card styling.


.card.active {
  opacity: 1;
  transform: scale(1) translateY(0);
  box-shadow: 0 30px 20px rgba(0, 0, 0, 0.2);
  z-index: 1;
}

Once a card becomes active, it should receive an opacity, scale up to a value of 1, and translate to a value of 0. This will give the card the appearance of shifting from the next position to the active position.

styled card carousel
styled card carousel


With these styles in place we finally have a card we can see! Clicking the Transition to Next button should be correctly rotating through the cards.

Let's add the styles for the next card.


.card.next {
  opacity: 1;
  z-index: 0;
}

Fortunately, there's not much to add here. We want the card to be visible, and we want the z-index value to be less than that of the active card.

Finally the styles for the previous card.


.card.prev {
  transform: scale(1.1) translateY(-50px);
  z-index: 2;
  opacity: 0;
  visibility: hidden;
}

When a card moves from active to previous, we want to scale it up slightly, as well as translate it upwards and fade it out via opacity: 0 and visibility: hidden. The important thing to note is that with the help of the prev class, we sort of hold this card in this state until it becomes "inactive" and thus able to achieve this effect.

Running on an Interval

Now that the carousel is working as we'd expect, let's make an adjustment so the cards rotate on an interval. To do this, we can add a useEffect hook to call the handleCardTransition function every four seconds.


useEffect(() => {
  const transitionInterval = setInterval(() => {
    handleCardTransition();
  }, 4000); // The interval value could also be a prop

	// Clear the interval when the component unmounts!
  return () => clearInterval(transitionInterval);
}, [handleCardTransition, indexes]);

Recall the useCallback hook we used earlier. That made it possible to call the handleCardTransition function like this from within of the useEffect hook. We could just as easily have moved all of that code into the setInterval function, but this structure allows for calling handleCardTransition in other scenarios. For example, if we wanted it to run on an interval AND to navigate the carousel cards via a button.

Summary

In this article, you learned how to build a React card carousel with CSS and React hooks. If you wanted to expand on this component, adding some previous/next controls and a pause button would be great additions!